diff --git a/api/v1beta1/slicegateway_types.go b/api/v1beta1/slicegateway_types.go index b4c9b4534..59bf0514b 100644 --- a/api/v1beta1/slicegateway_types.go +++ b/api/v1beta1/slicegateway_types.go @@ -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 diff --git a/config/crd/bases/networking.kubeslice.io_slicegateways.yaml b/config/crd/bases/networking.kubeslice.io_slicegateways.yaml index 90e552481..af08635f8 100644 --- a/config/crd/bases/networking.kubeslice.io_slicegateways.yaml +++ b/config/crd/bases/networking.kubeslice.io_slicegateways.yaml @@ -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 diff --git a/controllers/slicegateway/slicegateway.go b/controllers/slicegateway/slicegateway.go index c62b982a0..039037aeb 100644 --- a/controllers/slicegateway/slicegateway.go +++ b/controllers/slicegateway/slicegateway.go @@ -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. +// +// 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") @@ -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" @@ -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) diff --git a/controllers/slicegateway/slicegateway_route.go b/controllers/slicegateway/slicegateway_route.go new file mode 100644 index 000000000..b34a12135 --- /dev/null +++ b/controllers/slicegateway/slicegateway_route.go @@ -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 +} diff --git a/controllers/slicegateway/slicegateway_route_test.go b/controllers/slicegateway/slicegateway_route_test.go new file mode 100644 index 000000000..8fa37c7b0 --- /dev/null +++ b/controllers/slicegateway/slicegateway_route_test.go @@ -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) + } + }) + } +} diff --git a/go.mod b/go.mod index dd67f0aef..40d75f793 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index e7b3672f1..5187aec51 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/pkg/hub/controllers/slicegateway_config_test.go b/pkg/hub/controllers/slicegateway_config_test.go new file mode 100644 index 000000000..1ba83cc05 --- /dev/null +++ b/pkg/hub/controllers/slicegateway_config_test.go @@ -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") + } +} diff --git a/pkg/hub/controllers/slicegateway_controller.go b/pkg/hub/controllers/slicegateway_controller.go index b6222d240..bdd2dbe04 100644 --- a/pkg/hub/controllers/slicegateway_controller.go +++ b/pkg/hub/controllers/slicegateway_controller.go @@ -107,16 +107,7 @@ func (r *SliceGwReconciler) Reconcile(ctx context.Context, req reconcile.Request return reconcile.Result{}, err } // First check all the static fields. - if meshSliceGw.Status.Config.SliceGatewayID != sliceGw.Spec.LocalGatewayConfig.GatewayName || - meshSliceGw.Status.Config.SliceGatewaySubnet != sliceGw.Spec.LocalGatewayConfig.GatewaySubnet || - meshSliceGw.Status.Config.SliceGatewayRemoteSubnet != sliceGw.Spec.RemoteGatewayConfig.GatewaySubnet || - meshSliceGw.Status.Config.SliceGatewayHostType != sliceGw.Spec.GatewayHostType || - meshSliceGw.Status.Config.SliceGatewayRemoteClusterID != sliceGw.Spec.RemoteGatewayConfig.ClusterName || - meshSliceGw.Status.Config.SliceGatewayRemoteGatewayID != sliceGw.Spec.RemoteGatewayConfig.GatewayName || - meshSliceGw.Status.Config.SliceGatewayName != strconv.Itoa(sliceGw.Spec.GatewayNumber) || - meshSliceGw.Status.Config.SliceGatewayConnectivityType != sliceGw.Spec.GatewayConnectivityType || - meshSliceGw.Status.Config.SliceGatewayProtocol != sliceGw.Spec.GatewayProtocol || - meshSliceGw.Status.Config.SliceGatewayType != sliceGw.Spec.GatewayType { + if staticGatewayConfigChanged(meshSliceGw, sliceGw) { toUpdate = true } // If no change in static fields, check the dynamic fields @@ -148,25 +139,7 @@ func (r *SliceGwReconciler) Reconcile(ctx context.Context, req reconcile.Request if err != nil { return err } - meshSliceGw.Status.Config = kubeslicev1beta1.SliceGatewayConfig{ - SliceName: sliceGw.Spec.SliceName, - SliceGatewayID: sliceGw.Spec.LocalGatewayConfig.GatewayName, - SliceGatewaySubnet: sliceGw.Spec.LocalGatewayConfig.GatewaySubnet, - SliceGatewayRemoteSubnet: sliceGw.Spec.RemoteGatewayConfig.GatewaySubnet, - SliceGatewayHostType: sliceGw.Spec.GatewayHostType, - SliceGatewayRemoteNodeIPs: sliceGw.Spec.RemoteGatewayConfig.NodeIps, - SliceGatewayRemoteNodePorts: sliceGw.Spec.RemoteGatewayConfig.NodePorts, - SliceGatewayRemoteClusterID: sliceGw.Spec.RemoteGatewayConfig.ClusterName, - SliceGatewayRemoteGatewayID: sliceGw.Spec.RemoteGatewayConfig.GatewayName, - SliceGatewayLocalVpnIP: sliceGw.Spec.LocalGatewayConfig.VpnIp, - SliceGatewayRemoteVpnIP: sliceGw.Spec.RemoteGatewayConfig.VpnIp, - SliceGatewayName: strconv.Itoa(sliceGw.Spec.GatewayNumber), - SliceGatewayType: sliceGw.Spec.GatewayType, - SliceGatewayIntermediateDeployments: meshSliceGw.Status.Config.SliceGatewayIntermediateDeployments, - SliceGatewayConnectivityType: sliceGw.Spec.GatewayConnectivityType, - SliceGatewayProtocol: sliceGw.Spec.GatewayProtocol, - SliceGatewayServerLBIPs: sliceGw.Spec.RemoteGatewayConfig.LoadBalancerIps, - } + meshSliceGw.Status.Config = newMeshGatewayConfig(meshSliceGw, sliceGw) err = r.MeshClient.Status().Update(ctx, meshSliceGw) if err != nil { @@ -190,6 +163,52 @@ func (r *SliceGwReconciler) InjectClient(c client.Client) error { return nil } +// staticGatewayConfigChanged reports whether any static field of the local +// SliceGateway's reported config differs from the hub WorkerSliceGateway spec — +// including RouteEntireSliceSubnet, so a flag flip from the controller triggers +// an update on the worker. +func staticGatewayConfigChanged(meshSliceGw *kubeslicev1beta1.SliceGateway, sliceGw *spokev1alpha1.WorkerSliceGateway) bool { + c := meshSliceGw.Status.Config + return c.SliceGatewayID != sliceGw.Spec.LocalGatewayConfig.GatewayName || + c.SliceGatewaySubnet != sliceGw.Spec.LocalGatewayConfig.GatewaySubnet || + c.SliceGatewayRemoteSubnet != sliceGw.Spec.RemoteGatewayConfig.GatewaySubnet || + c.SliceGatewayHostType != sliceGw.Spec.GatewayHostType || + c.SliceGatewayRemoteClusterID != sliceGw.Spec.RemoteGatewayConfig.ClusterName || + c.SliceGatewayRemoteGatewayID != sliceGw.Spec.RemoteGatewayConfig.GatewayName || + c.SliceGatewayName != strconv.Itoa(sliceGw.Spec.GatewayNumber) || + c.SliceGatewayConnectivityType != sliceGw.Spec.GatewayConnectivityType || + c.SliceGatewayProtocol != sliceGw.Spec.GatewayProtocol || + c.RouteEntireSliceSubnet != sliceGw.Spec.RouteEntireSliceSubnet || + c.SliceGatewayType != sliceGw.Spec.GatewayType +} + +// newMeshGatewayConfig builds the local SliceGateway status config from the hub +// WorkerSliceGateway spec, preserving the existing intermediate deployments. The +// controller-set RouteEntireSliceSubnet flag is propagated here so the worker +// dataplane can route the whole slice via a spoke's hub gateway. +func newMeshGatewayConfig(meshSliceGw *kubeslicev1beta1.SliceGateway, sliceGw *spokev1alpha1.WorkerSliceGateway) kubeslicev1beta1.SliceGatewayConfig { + return kubeslicev1beta1.SliceGatewayConfig{ + SliceName: sliceGw.Spec.SliceName, + SliceGatewayID: sliceGw.Spec.LocalGatewayConfig.GatewayName, + SliceGatewaySubnet: sliceGw.Spec.LocalGatewayConfig.GatewaySubnet, + SliceGatewayRemoteSubnet: sliceGw.Spec.RemoteGatewayConfig.GatewaySubnet, + SliceGatewayHostType: sliceGw.Spec.GatewayHostType, + SliceGatewayRemoteNodeIPs: sliceGw.Spec.RemoteGatewayConfig.NodeIps, + SliceGatewayRemoteNodePorts: sliceGw.Spec.RemoteGatewayConfig.NodePorts, + SliceGatewayRemoteClusterID: sliceGw.Spec.RemoteGatewayConfig.ClusterName, + SliceGatewayRemoteGatewayID: sliceGw.Spec.RemoteGatewayConfig.GatewayName, + SliceGatewayLocalVpnIP: sliceGw.Spec.LocalGatewayConfig.VpnIp, + SliceGatewayRemoteVpnIP: sliceGw.Spec.RemoteGatewayConfig.VpnIp, + SliceGatewayName: strconv.Itoa(sliceGw.Spec.GatewayNumber), + SliceGatewayType: sliceGw.Spec.GatewayType, + SliceGatewayIntermediateDeployments: meshSliceGw.Status.Config.SliceGatewayIntermediateDeployments, + SliceGatewayConnectivityType: sliceGw.Spec.GatewayConnectivityType, + SliceGatewayProtocol: sliceGw.Spec.GatewayProtocol, + SliceGatewayServerLBIPs: sliceGw.Spec.RemoteGatewayConfig.LoadBalancerIps, + RouteEntireSliceSubnet: sliceGw.Spec.RouteEntireSliceSubnet, + } +} + func (r *SliceGwReconciler) createSliceGwCerts(ctx context.Context, sliceGw *spokev1alpha1.WorkerSliceGateway, req reconcile.Request) (reconcile.Result, error) { log := logger.FromContext(ctx) meshSliceGwCerts := &corev1.Secret{} diff --git a/vendor/github.com/kubeslice/apis/pkg/controller/v1alpha1/cluster_types.go b/vendor/github.com/kubeslice/apis/pkg/controller/v1alpha1/cluster_types.go index dda4a7f21..e9b415d99 100644 --- a/vendor/github.com/kubeslice/apis/pkg/controller/v1alpha1/cluster_types.go +++ b/vendor/github.com/kubeslice/apis/pkg/controller/v1alpha1/cluster_types.go @@ -137,6 +137,30 @@ type ClusterStatus struct { // VCPURestriction is the restriction on the cluster disabling the creation of new pods VCPURestriction *VCPURestriction `json:"vCPURestriction,omitempty"` GPURestriction *GPURestriction `json:"GPURestriction,omitempty"` + // StorageCapabilities contains auto-detected storage capabilities reported by the worker operator. + // Populated only when the worker operator's storage-capability reconciler is active. + StorageCapabilities *StorageCapabilities `json:"storageCapabilities,omitempty"` +} + +// StorageCapabilities holds auto-detected RWX-capable storage classes on the worker cluster. +// To add support for a new storage system, append its CSI provisioner string to the +// worker operator's rwxProvisioners list — no changes to this struct are required. +type StorageCapabilities struct { + // RWXStorageClasses lists all ReadWriteMany-capable StorageClasses detected on the cluster + RWXStorageClasses []RWXStorageClass `json:"rwxStorageClasses,omitempty"` + // DefaultStorageClass is the name of the StorageClass annotated with + // storageclass.kubernetes.io/is-default-class: "true" on the worker cluster + DefaultStorageClass string `json:"defaultStorageClass,omitempty"` + // LastUpdated is the timestamp when capabilities were last detected + LastUpdated metav1.Time `json:"lastUpdated,omitempty"` +} + +// RWXStorageClass describes a single ReadWriteMany-capable StorageClass. +type RWXStorageClass struct { + // Name is the StorageClass name (e.g. "rook-cephfs", "juicefs-sc") + Name string `json:"name"` + // Provisioner is the CSI provisioner string (e.g. "rook-ceph.cephfs.csi.ceph.com") + Provisioner string `json:"provisioner"` } type GPURestriction struct { diff --git a/vendor/github.com/kubeslice/apis/pkg/controller/v1alpha1/zz_generated.deepcopy.go b/vendor/github.com/kubeslice/apis/pkg/controller/v1alpha1/zz_generated.deepcopy.go index baeea08ce..abd79aede 100644 --- a/vendor/github.com/kubeslice/apis/pkg/controller/v1alpha1/zz_generated.deepcopy.go +++ b/vendor/github.com/kubeslice/apis/pkg/controller/v1alpha1/zz_generated.deepcopy.go @@ -175,6 +175,11 @@ func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { *out = new(GPURestriction) (*in).DeepCopyInto(*out) } + if in.StorageCapabilities != nil { + in, out := &in.StorageCapabilities, &out.StorageCapabilities + *out = new(StorageCapabilities) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. @@ -473,6 +478,21 @@ func (in *QOSProfile) DeepCopy() *QOSProfile { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RWXStorageClass) DeepCopyInto(out *RWXStorageClass) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RWXStorageClass. +func (in *RWXStorageClass) DeepCopy() *RWXStorageClass { + if in == nil { + return nil + } + out := new(RWXStorageClass) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceAccess) DeepCopyInto(out *ServiceAccess) { *out = *in @@ -917,6 +937,27 @@ func (in *StatusOfKeyRotation) DeepCopy() *StatusOfKeyRotation { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StorageCapabilities) DeepCopyInto(out *StorageCapabilities) { + *out = *in + if in.RWXStorageClasses != nil { + in, out := &in.RWXStorageClasses, &out.RWXStorageClasses + *out = make([]RWXStorageClass, len(*in)) + copy(*out, *in) + } + in.LastUpdated.DeepCopyInto(&out.LastUpdated) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageCapabilities. +func (in *StorageCapabilities) DeepCopy() *StorageCapabilities { + if in == nil { + return nil + } + out := new(StorageCapabilities) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Telemetry) DeepCopyInto(out *Telemetry) { *out = *in diff --git a/vendor/github.com/kubeslice/apis/pkg/worker/v1alpha1/workerslicegateway_types.go b/vendor/github.com/kubeslice/apis/pkg/worker/v1alpha1/workerslicegateway_types.go index c9a8b2b85..20076f6ff 100644 --- a/vendor/github.com/kubeslice/apis/pkg/worker/v1alpha1/workerslicegateway_types.go +++ b/vendor/github.com/kubeslice/apis/pkg/worker/v1alpha1/workerslicegateway_types.go @@ -41,6 +41,12 @@ type WorkerSliceGatewaySpec struct { LocalGatewayConfig SliceGatewayConfig `json:"localGatewayConfig,omitempty"` RemoteGatewayConfig SliceGatewayConfig `json:"remoteGatewayConfig,omitempty"` GatewayNumber int `json:"gatewayNumber,omitempty"` + // RouteEntireSliceSubnet, when true, tells the worker to route the whole + // slice subnet (not just the peer gateway's subnet) via this gateway. The + // controller sets it on a spoke's gateway to the hub in HubAndSpoke topology, + // so a spoke forwards all slice-internal traffic (including traffic destined + // for other spokes) to the hub, which relays it. + RouteEntireSliceSubnet bool `json:"routeEntireSliceSubnet,omitempty"` } type SliceGatewayConfig struct { @@ -61,9 +67,32 @@ type GatewayCredentials struct { } // WorkerSliceGatewayStatus defines the observed state of WorkerSliceGateway +// Gateway connection states reported by the worker on WorkerSliceGatewayStatus. +const ( + // GatewayConnectionStateConnected means the gateway tunnel is up (at least + // one HA gateway pod reports its tunnel established). + GatewayConnectionStateConnected = "Connected" + // GatewayConnectionStateNotConnected means the tunnel is down (all gateway + // pods report their tunnel not established). + GatewayConnectionStateNotConnected = "NotConnected" + // GatewayConnectionStatePending means no connectivity has been reported yet + // (e.g. the gateway was just created). An empty ConnectionState is treated + // as Pending by the controller-side aggregation. + GatewayConnectionStatePending = "Pending" +) + type WorkerSliceGatewayStatus struct { GatewayNumber int `json:"gatewayNumber,omitempty"` ClusterInsertionIndex int `json:"clusterInsertionIndex,omitempty"` + // ConnectionState is the connectivity state of this gateway link as reported + // by the worker: Connected, NotConnected or Pending. Empty means Pending. + ConnectionState string `json:"connectionState,omitempty"` + // LastTransitionTime is the time ConnectionState last changed. + LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty"` + // Reason is a short, machine-readable reason for the current ConnectionState. + Reason string `json:"reason,omitempty"` + // Message is a human-readable description of the current ConnectionState. + Message string `json:"message,omitempty"` } //+kubebuilder:object:root=true diff --git a/vendor/github.com/kubeslice/apis/pkg/worker/v1alpha1/zz_generated.deepcopy.go b/vendor/github.com/kubeslice/apis/pkg/worker/v1alpha1/zz_generated.deepcopy.go index 5cd610f7b..4be280013 100644 --- a/vendor/github.com/kubeslice/apis/pkg/worker/v1alpha1/zz_generated.deepcopy.go +++ b/vendor/github.com/kubeslice/apis/pkg/worker/v1alpha1/zz_generated.deepcopy.go @@ -496,7 +496,7 @@ func (in *WorkerSliceGateway) DeepCopyInto(out *WorkerSliceGateway) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerSliceGateway. @@ -585,6 +585,10 @@ func (in *WorkerSliceGatewaySpec) DeepCopy() *WorkerSliceGatewaySpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WorkerSliceGatewayStatus) DeepCopyInto(out *WorkerSliceGatewayStatus) { *out = *in + if in.LastTransitionTime != nil { + in, out := &in.LastTransitionTime, &out.LastTransitionTime + *out = (*in).DeepCopy() + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerSliceGatewayStatus. diff --git a/vendor/modules.txt b/vendor/modules.txt index 21e06e176..f47fb5309 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -116,7 +116,7 @@ github.com/josharian/intern # github.com/json-iterator/go v1.1.12 ## explicit; go 1.12 github.com/json-iterator/go -# github.com/kubeslice/apis v0.4.0 +# github.com/kubeslice/apis v0.4.0 => github.com/Shreesha001/apis v0.0.0-20260716162233-4dfda414c6d2 ## explicit; go 1.24.0 github.com/kubeslice/apis/pkg/controller/v1alpha1 github.com/kubeslice/apis/pkg/worker/v1alpha1 @@ -977,3 +977,4 @@ sigs.k8s.io/structured-merge-diff/v4/value ## explicit; go 1.12 sigs.k8s.io/yaml sigs.k8s.io/yaml/goyaml.v2 +# github.com/kubeslice/apis => github.com/Shreesha001/apis v0.0.0-20260716162233-4dfda414c6d2