Skip to content
Draft
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
4 changes: 4 additions & 0 deletions api/v1beta1/slicegateway_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ type SliceGatewayConfig struct {
SliceGatewayRemoteClusterID string `json:"sliceGatewayRemoteClusterId,omitempty"`
// Intermediate Slice Gw Deployments
SliceGatewayIntermediateDeployments []string `json:"sliceGatewayIntermediateDeployments,omitempty"`
// RouteEntireSliceSubnet, when true, tells the slice router to route the whole
// slice subnet via this gateway (a spoke's uplink to the hub in HubAndSpoke
// topology), so spoke-to-spoke traffic is relayed through the hub.
RouteEntireSliceSubnet bool `json:"routeEntireSliceSubnet,omitempty"`
// SliceGateway Connectivity Type
SliceGatewayConnectivityType string `json:"sliceGatewayConnectivityType,omitempty"`
// SliceGateway Protocol Type: UDP or TCP
Expand Down
6 changes: 6 additions & 0 deletions config/crd/bases/networking.kubeslice.io_slicegateways.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ spec:
config:
description: SliceGatewayConfig defines the config received from backend
properties:
routeEntireSliceSubnet:
description: |-
RouteEntireSliceSubnet, when true, tells the slice router to route the whole
slice subnet via this gateway (a spoke's uplink to the hub in HubAndSpoke
topology), so spoke-to-spoke traffic is relayed through the hub.
type: boolean
sliceGatewayConnectivityType:
description: SliceGateway Connectivity Type
type: string
Expand Down
63 changes: 61 additions & 2 deletions controllers/slicegateway/slicegateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,42 @@ func (r *SliceGwReconciler) ReconcileGwPodStatus(ctx context.Context, slicegatew
return ctrl.Result{}, nil, false
}

// remoteSubnetForGateway returns the destination subnet that traffic crossing
// this gateway to its peer should be routed to. Normally it is the peer
// gateway's own subnet (SliceGatewayRemoteSubnet). For a spoke's gateway to the
// hub in a HubAndSpoke topology (RouteEntireSliceSubnet), it is the entire slice
// subnet, so the spoke forwards all slice-internal traffic - including traffic
// destined for other spokes - to the hub, which relays it.
//
// The returned subnet is programmed both into the local slice router (so pods
// reach the gateway) and into the gateway pod itself (so it forwards the traffic
// over the tunnel); both must agree, otherwise packets loop at the gateway.
//
Comment on lines +951 to +954
// ready is false when the entire-slice route is requested but the slice subnet
// is not known yet, signalling the caller to requeue.
func (r *SliceGwReconciler) remoteSubnetForGateway(ctx context.Context, slicegateway *kubeslicev1beta1.SliceGateway) (string, bool, error) {
routeEntireSlice := slicegateway.Status.Config.RouteEntireSliceSubnet
gatewayRemoteSubnet := slicegateway.Status.Config.SliceGatewayRemoteSubnet

// Only the entire-slice case needs the slice subnet; the common (peer-subnet)
// case avoids the extra lookup entirely.
if !routeEntireSlice {
subnet, ready := remoteNsmSubnetForRoute(false, gatewayRemoteSubnet, "")
return subnet, ready, nil
}

slice, err := controllers.GetSlice(ctx, r.Client, slicegateway.Spec.SliceName)
if err != nil {
return "", false, err
}
sliceSubnet := ""
if slice != nil && slice.Status.SliceConfig != nil {
sliceSubnet = slice.Status.SliceConfig.SliceSubnet
}
subnet, ready := remoteNsmSubnetForRoute(true, gatewayRemoteSubnet, sliceSubnet)
return subnet, ready, nil
}

func (r *SliceGwReconciler) SendConnectionContextAndQosToGwPod(ctx context.Context, slice *kubeslicev1beta1.Slice, slicegateway *kubeslicev1beta1.SliceGateway, req reconcile.Request) (ctrl.Result, error, bool) {
log := logger.FromContext(ctx).WithValues("type", "SliceGw")

Expand All @@ -953,9 +989,20 @@ func (r *SliceGwReconciler) SendConnectionContextAndQosToGwPod(ctx context.Conte
log.Info("Gw podIPs not available yet, requeuing")
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil, true
}
// The gateway pod must forward the same subnet the slice router hands it,
// otherwise (for spoke-to-spoke) the packet loops back to the slice router.
remoteSubnet, ready, err := r.remoteSubnetForGateway(ctx, slicegateway)
if err != nil {
log.Error(err, "Unable to get slice for entire-subnet route", "slice", slicegateway.Spec.SliceName)
return ctrl.Result{}, err, true
}
if !ready {
log.Info("Slice subnet not available yet for entire-subnet route, requeuing")
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil, true
}
connCtx := &gwsidecar.GwConnectionContext{
RemoteSliceGwVpnIP: slicegateway.Status.Config.SliceGatewayRemoteVpnIP,
RemoteSliceGwNsmSubnet: slicegateway.Status.Config.SliceGatewayRemoteSubnet,
RemoteSliceGwNsmSubnet: remoteSubnet,
}
for i := range gwPodsInfo {
sidecarGrpcAddress := gwPodsInfo[i].PodIP + ":5000"
Expand Down Expand Up @@ -1026,8 +1073,20 @@ func (r *SliceGwReconciler) SendConnectionContextToSliceRouter(ctx context.Conte
}

sidecarGrpcAddress := podIP + ":5000"
// The slice router and the gateway pod must be programmed with the same
// remote subnet; for a spoke->hub gateway this is the entire slice subnet so
// spoke-to-spoke traffic is forwarded to the hub for relaying.
remoteNsmSubnet, ready, err := r.remoteSubnetForGateway(ctx, slicegateway)
if err != nil {
log.Error(err, "Unable to get slice for entire-subnet route", "slice", slicegateway.Spec.SliceName)
return ctrl.Result{}, err, true
}
if !ready {
log.Info("Slice subnet not available yet for entire-subnet route, requeuing")
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil, true
}
connCtx := &router.SliceRouterConnCtx{
RemoteSliceGwNsmSubnet: slicegateway.Status.Config.SliceGatewayRemoteSubnet,
RemoteSliceGwNsmSubnet: remoteNsmSubnet,
LocalNsmGwPeerIPs: gwNsmIPs,
}
log.Info("Conn ctx to send to slice router ", "connCtx", connCtx)
Expand Down
35 changes: 35 additions & 0 deletions controllers/slicegateway/slicegateway_route.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2026 Avesha, Inc. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* 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 slicegateway

// remoteNsmSubnetForRoute decides which subnet the slice router should route via
// a gateway. Normally it is the peer gateway's subnet. For a spoke's gateway to
// the hub (routeEntireSliceSubnet), it is the entire slice subnet, so the spoke
// forwards all slice-internal traffic (including traffic for other spokes) to the
// hub. ready is false when the entire-slice route is requested but the slice
// subnet is not known yet, signalling the caller to requeue.
func remoteNsmSubnetForRoute(routeEntireSliceSubnet bool, gatewayRemoteSubnet, sliceSubnet string) (subnet string, ready bool) {
if !routeEntireSliceSubnet {
return gatewayRemoteSubnet, true
}
if sliceSubnet == "" {
return "", false
}
return sliceSubnet, true
}
64 changes: 64 additions & 0 deletions controllers/slicegateway/slicegateway_route_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2026 Avesha, Inc. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* 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 slicegateway

import "testing"

func TestRemoteNsmSubnetForRoute(t *testing.T) {
cases := []struct {
name string
routeEntireSliceSubnet bool
gatewayRemoteSubnet string
sliceSubnet string
wantSubnet string
wantReady bool
}{
{
name: "full-mesh/normal gateway uses peer gateway subnet",
gatewayRemoteSubnet: "10.1.1.0/24",
sliceSubnet: "10.1.0.0/16",
wantSubnet: "10.1.1.0/24",
wantReady: true,
},
{
name: "spoke-to-hub gateway uses entire slice subnet",
routeEntireSliceSubnet: true,
gatewayRemoteSubnet: "10.1.1.0/24",
sliceSubnet: "10.1.0.0/16",
wantSubnet: "10.1.0.0/16",
wantReady: true,
},
{
name: "spoke-to-hub gateway not ready when slice subnet unknown",
routeEntireSliceSubnet: true,
gatewayRemoteSubnet: "10.1.1.0/24",
sliceSubnet: "",
wantSubnet: "",
wantReady: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
subnet, ready := remoteNsmSubnetForRoute(tc.routeEntireSliceSubnet, tc.gatewayRemoteSubnet, tc.sliceSubnet)
if subnet != tc.wantSubnet || ready != tc.wantReady {
t.Fatalf("got (%q, %v), want (%q, %v)", subnet, ready, tc.wantSubnet, tc.wantReady)
}
})
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,5 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

replace github.com/kubeslice/apis => github.com/Shreesha001/apis v0.0.0-20260716162233-4dfda414c6d2
Comment on lines +105 to +106
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shreesha001/apis v0.0.0-20260716162233-4dfda414c6d2 h1:GraUvpBfFWegugw7Pbxtuv4pC3z4rv67uTTZWrmpmb4=
github.com/Shreesha001/apis v0.0.0-20260716162233-4dfda414c6d2/go.mod h1:F1hXnAt3Dk4Sto5yQDoMnqgXX5ImL1bRBiAmrW6TG00=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
Expand Down Expand Up @@ -264,8 +266,6 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kubeslice/apis v0.4.0 h1:nU66JoA2OQx48bZnXDWH8iHG+5R1ELX5ikc3l/fn5II=
github.com/kubeslice/apis v0.4.0/go.mod h1:F1hXnAt3Dk4Sto5yQDoMnqgXX5ImL1bRBiAmrW6TG00=
github.com/kubeslice/gateway-sidecar v0.2.0 h1:Ja3fIUivuSjUFQ4lPCt79ATq99BxslvAFYUwV9Urpy4=
github.com/kubeslice/gateway-sidecar v0.2.0/go.mod h1:nM1+Wjud2vk44cUg+9iwBbWTpqI+2Ecbn9NuaHEs9aY=
github.com/kubeslice/kubeslice-monitoring v0.2.1 h1:wtmIEigpQoKzuckof7QRqdsaa4lV/rqxd/FcmOj5N5Q=
Expand Down
88 changes: 88 additions & 0 deletions pkg/hub/controllers/slicegateway_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2026 Avesha, Inc. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* 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 controllers

import (
"testing"

spokev1alpha1 "github.com/kubeslice/apis/pkg/worker/v1alpha1"
kubeslicev1beta1 "github.com/kubeslice/worker-operator/api/v1beta1"
)

// sampleHubGateway returns a hub WorkerSliceGateway spec with all the fields the
// config propagation reads, with RouteEntireSliceSubnet set as requested.
func sampleHubGateway(route bool) *spokev1alpha1.WorkerSliceGateway {
gw := &spokev1alpha1.WorkerSliceGateway{}
gw.Spec.SliceName = "slice"
gw.Spec.GatewayType = "OpenVPN"
gw.Spec.GatewayHostType = "Client"
gw.Spec.GatewayConnectivityType = "NodePort"
gw.Spec.GatewayProtocol = "UDP"
gw.Spec.GatewayNumber = 1
gw.Spec.RouteEntireSliceSubnet = route
gw.Spec.LocalGatewayConfig = spokev1alpha1.SliceGatewayConfig{
GatewayName: "gw-1", GatewaySubnet: "10.11.16.0/20", VpnIp: "10.11.255.2",
}
gw.Spec.RemoteGatewayConfig = spokev1alpha1.SliceGatewayConfig{
GatewayName: "gw-2", GatewaySubnet: "10.11.0.0/20", ClusterName: "worker-1", VpnIp: "10.11.255.1",
}
return gw
}

// TestNewMeshGatewayConfig_PropagatesRouteEntireSliceSubnet verifies the
// controller-set RouteEntireSliceSubnet flag is copied onto the local
// SliceGateway status config, in both states, along with a couple of other
// fields as a sanity check.
func TestNewMeshGatewayConfig_PropagatesRouteEntireSliceSubnet(t *testing.T) {
mesh := &kubeslicev1beta1.SliceGateway{}
for _, route := range []bool{true, false} {
cfg := newMeshGatewayConfig(mesh, sampleHubGateway(route))
if cfg.RouteEntireSliceSubnet != route {
t.Errorf("RouteEntireSliceSubnet: got %v, want %v", cfg.RouteEntireSliceSubnet, route)
}
if cfg.SliceName != "slice" {
t.Errorf("SliceName not propagated: got %q", cfg.SliceName)
}
if string(cfg.SliceGatewayType) != "OpenVPN" {
t.Errorf("SliceGatewayType not propagated: got %q", cfg.SliceGatewayType)
}
if cfg.SliceGatewayRemoteSubnet != "10.11.0.0/20" {
t.Errorf("SliceGatewayRemoteSubnet not propagated: got %q", cfg.SliceGatewayRemoteSubnet)
}
}
}

// TestStaticGatewayConfigChanged_DetectsRouteFlag verifies the change-detection
// treats a RouteEntireSliceSubnet flip as a change (so the worker re-syncs when
// the controller toggles the flag), and reports no change when everything
// already matches.
func TestStaticGatewayConfigChanged_DetectsRouteFlag(t *testing.T) {
hub := sampleHubGateway(true)
mesh := &kubeslicev1beta1.SliceGateway{}
// seed the local config to exactly match the hub spec → no change expected
mesh.Status.Config = newMeshGatewayConfig(mesh, hub)
if staticGatewayConfigChanged(mesh, hub) {
t.Fatal("expected no change when local config already matches the hub spec")
}
// flip only the flag on the hub spec → must be detected as a change
hub.Spec.RouteEntireSliceSubnet = false
if !staticGatewayConfigChanged(mesh, hub) {
t.Fatal("expected a RouteEntireSliceSubnet flip to be detected as a change")
}
}
Loading