From a4634c4eeb2b0337877b43456075aa755ecfb761 Mon Sep 17 00:00:00 2001 From: Shreesha001 Date: Thu, 16 Jul 2026 20:17:31 +0530 Subject: [PATCH 1/3] feat: report gateway tunnel connectivity to hub WorkerSliceGateway status Signed-off-by: Shreesha001 --- .../controllers/slicegateway_controller.go | 11 +- pkg/hub/controllers/slicegateway_status.go | 81 ++++++++++ .../controllers/slicegateway_status_test.go | 140 ++++++++++++++++++ 3 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 pkg/hub/controllers/slicegateway_status.go create mode 100644 pkg/hub/controllers/slicegateway_status_test.go diff --git a/pkg/hub/controllers/slicegateway_controller.go b/pkg/hub/controllers/slicegateway_controller.go index b6222d240..b98f53248 100644 --- a/pkg/hub/controllers/slicegateway_controller.go +++ b/pkg/hub/controllers/slicegateway_controller.go @@ -182,7 +182,16 @@ func (r *SliceGwReconciler) Reconcile(ctx context.Context, req reconcile.Request } } - return reconcile.Result{}, nil + // Report this gateway's tunnel connectivity up to the hub WorkerSliceGateway + // so the controller can aggregate slice-level topology convergence. + if err := r.reconcileGatewayConnectionStatus(ctx, sliceGw, meshSliceGw); err != nil { + log.Error(err, "unable to update gateway connection status on hub", "sliceGw", sliceGwName) + return reconcile.Result{}, err + } + + // The hub reconciler does not watch the mesh cluster's SliceGateway, so + // periodically re-reconcile to pick up tunnel connectivity changes. + return reconcile.Result{RequeueAfter: gatewayStatusRefreshInterval}, nil } func (r *SliceGwReconciler) InjectClient(c client.Client) error { diff --git a/pkg/hub/controllers/slicegateway_status.go b/pkg/hub/controllers/slicegateway_status.go new file mode 100644 index 000000000..025bb7515 --- /dev/null +++ b/pkg/hub/controllers/slicegateway_status.go @@ -0,0 +1,81 @@ +/* + * 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 ( + "context" + "time" + + spokev1alpha1 "github.com/kubeslice/apis/pkg/worker/v1alpha1" + kubeslicev1beta1 "github.com/kubeslice/worker-operator/api/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// tunnelStateUp is the value gateway-sidecar reports (via getTunnelState) on a +// gateway pod whose tunnel is established. It mirrors the "UP" string set on +// SliceGateway.Status.GatewayPodStatus[].TunnelStatus.TunnelState. +const tunnelStateUp = "UP" + +// gatewayStatusRefreshInterval is how often the hub reconciler re-checks the +// local SliceGateway tunnel status and reports it up, since it does not watch +// the mesh cluster directly. +const gatewayStatusRefreshInterval = 30 * time.Second + +// deriveGatewayConnectionState aggregates the per-pod tunnel states of a local +// SliceGateway into a single WorkerSliceGateway connection state. It is HA-aware: +// the gateway is Connected when at least one pod's tunnel is up, NotConnected +// when all pods are down, and Pending when no pod status has been reported yet. +func deriveGatewayConnectionState(pods []*kubeslicev1beta1.GwPodInfo) string { + if len(pods) == 0 { + return spokev1alpha1.GatewayConnectionStatePending + } + for _, pod := range pods { + if pod != nil && pod.TunnelStatus.TunnelState == tunnelStateUp { + return spokev1alpha1.GatewayConnectionStateConnected + } + } + return spokev1alpha1.GatewayConnectionStateNotConnected +} + +// reconcileGatewayConnectionStatus derives the gateway's connection state from +// the local SliceGateway's pod tunnel status and, when it has changed, writes it +// to the WorkerSliceGateway.status on the hub so the controller can aggregate +// slice-level topology convergence. The write is guarded against conflicts by +// re-fetching the latest object and retrying. +func (r *SliceGwReconciler) reconcileGatewayConnectionStatus(ctx context.Context, sliceGw *spokev1alpha1.WorkerSliceGateway, meshSliceGw *kubeslicev1beta1.SliceGateway) error { + state := deriveGatewayConnectionState(meshSliceGw.Status.GatewayPodStatus) + if sliceGw.Status.ConnectionState == state { + return nil + } + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + latest := &spokev1alpha1.WorkerSliceGateway{} + if err := r.Get(ctx, client.ObjectKey{Name: sliceGw.Name, Namespace: sliceGw.Namespace}, latest); err != nil { + return err + } + if latest.Status.ConnectionState == state { + return nil + } + now := metav1.Now() + latest.Status.ConnectionState = state + latest.Status.LastTransitionTime = &now + return r.Status().Update(ctx, latest) + }) +} diff --git a/pkg/hub/controllers/slicegateway_status_test.go b/pkg/hub/controllers/slicegateway_status_test.go new file mode 100644 index 000000000..a88cae8d5 --- /dev/null +++ b/pkg/hub/controllers/slicegateway_status_test.go @@ -0,0 +1,140 @@ +/* + * 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 ( + "context" + "testing" + + spokev1alpha1 "github.com/kubeslice/apis/pkg/worker/v1alpha1" + kubeslicev1beta1 "github.com/kubeslice/worker-operator/api/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func pod(state string) *kubeslicev1beta1.GwPodInfo { + return &kubeslicev1beta1.GwPodInfo{TunnelStatus: kubeslicev1beta1.TunnelStatus{TunnelState: state}} +} + +func TestDeriveGatewayConnectionState(t *testing.T) { + cases := []struct { + name string + pods []*kubeslicev1beta1.GwPodInfo + want string + }{ + { + name: "no pod status is Pending", + pods: nil, + want: spokev1alpha1.GatewayConnectionStatePending, + }, + { + name: "all pods up is Connected", + pods: []*kubeslicev1beta1.GwPodInfo{pod("UP"), pod("UP")}, + want: spokev1alpha1.GatewayConnectionStateConnected, + }, + { + name: "at least one pod up is Connected (HA)", + pods: []*kubeslicev1beta1.GwPodInfo{pod("DOWN"), pod("UP")}, + want: spokev1alpha1.GatewayConnectionStateConnected, + }, + { + name: "all pods down is NotConnected", + pods: []*kubeslicev1beta1.GwPodInfo{pod("DOWN"), pod("DOWN")}, + want: spokev1alpha1.GatewayConnectionStateNotConnected, + }, + { + name: "unknown/empty pod states are not up", + pods: []*kubeslicev1beta1.GwPodInfo{pod("UNKNOWN"), pod("")}, + want: spokev1alpha1.GatewayConnectionStateNotConnected, + }, + { + name: "nil pod entries are ignored", + pods: []*kubeslicev1beta1.GwPodInfo{nil, pod("UP")}, + want: spokev1alpha1.GatewayConnectionStateConnected, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := deriveGatewayConnectionState(tc.pods) + if got != tc.want { + t.Fatalf("%s: got %q, want %q", tc.name, got, tc.want) + } + }) + } +} + +func meshGwWithPods(states ...string) *kubeslicev1beta1.SliceGateway { + mesh := &kubeslicev1beta1.SliceGateway{} + for _, s := range states { + mesh.Status.GatewayPodStatus = append(mesh.Status.GatewayPodStatus, pod(s)) + } + return mesh +} + +func TestReconcileGatewayConnectionStatus(t *testing.T) { + scheme := runtime.NewScheme() + if err := spokev1alpha1.AddToScheme(scheme); err != nil { + t.Fatalf("add scheme: %v", err) + } + key := types.NamespacedName{Name: "slice-hub-spoke1", Namespace: "kubeslice-project"} + + t.Run("writes Connected when a tunnel is up", func(t *testing.T) { + gw := &spokev1alpha1.WorkerSliceGateway{} + gw.Name, gw.Namespace = key.Name, key.Namespace + c := fake.NewClientBuilder().WithScheme(scheme). + WithObjects(gw).WithStatusSubresource(gw).Build() + r := &SliceGwReconciler{Client: c} + + if err := r.reconcileGatewayConnectionStatus(context.Background(), gw, meshGwWithPods("UP")); err != nil { + t.Fatalf("reconcile: %v", err) + } + got := &spokev1alpha1.WorkerSliceGateway{} + if err := c.Get(context.Background(), key, got); err != nil { + t.Fatalf("get: %v", err) + } + if got.Status.ConnectionState != spokev1alpha1.GatewayConnectionStateConnected { + t.Fatalf("connectionState = %q, want Connected", got.Status.ConnectionState) + } + if got.Status.LastTransitionTime == nil { + t.Fatal("expected LastTransitionTime to be set on transition") + } + }) + + t.Run("no write when state is unchanged", func(t *testing.T) { + gw := &spokev1alpha1.WorkerSliceGateway{} + gw.Name, gw.Namespace = key.Name, key.Namespace + gw.Status.ConnectionState = spokev1alpha1.GatewayConnectionStateNotConnected + c := fake.NewClientBuilder().WithScheme(scheme). + WithObjects(gw).WithStatusSubresource(gw).Build() + r := &SliceGwReconciler{Client: c} + + // all pods down -> NotConnected, same as current -> no update, no timestamp. + if err := r.reconcileGatewayConnectionStatus(context.Background(), gw, meshGwWithPods("DOWN")); err != nil { + t.Fatalf("reconcile: %v", err) + } + got := &spokev1alpha1.WorkerSliceGateway{} + if err := c.Get(context.Background(), key, got); err != nil { + t.Fatalf("get: %v", err) + } + if got.Status.LastTransitionTime != nil { + t.Fatal("expected no LastTransitionTime when state is unchanged") + } + }) +} From 05fced262ab3aa45bbba5ca43f980f83b5921742 Mon Sep 17 00:00:00 2001 From: Shreesha001 Date: Sat, 1 Aug 2026 00:45:23 +0530 Subject: [PATCH 2/3] build: point apis replace at fork ref so #471 connection-state constants resolve Signed-off-by: Shreesha001 --- go.mod | 2 + go.sum | 4 +- .../pkg/controller/v1alpha1/cluster_types.go | 24 +++++++++++ .../v1alpha1/zz_generated.deepcopy.go | 41 +++++++++++++++++++ .../v1alpha1/workerslicegateway_types.go | 29 +++++++++++++ .../worker/v1alpha1/zz_generated.deepcopy.go | 6 ++- vendor/modules.txt | 3 +- 7 files changed, 105 insertions(+), 4 deletions(-) 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/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 From c4d8ba49018010021a9765a5f02d7def6975a442 Mon Sep 17 00:00:00 2001 From: Shreesha001 Date: Sat, 8 Aug 2026 12:38:06 +0530 Subject: [PATCH 3/3] feat: populate gateway ConnectionState Reason and Message status fields Signed-off-by: Shreesha001 --- pkg/hub/controllers/slicegateway_status.go | 32 ++++++++++++++++--- .../controllers/slicegateway_status_test.go | 15 +++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/pkg/hub/controllers/slicegateway_status.go b/pkg/hub/controllers/slicegateway_status.go index 025bb7515..d8d175d24 100644 --- a/pkg/hub/controllers/slicegateway_status.go +++ b/pkg/hub/controllers/slicegateway_status.go @@ -60,9 +60,27 @@ func deriveGatewayConnectionState(pods []*kubeslicev1beta1.GwPodInfo) string { // to the WorkerSliceGateway.status on the hub so the controller can aggregate // slice-level topology convergence. The write is guarded against conflicts by // re-fetching the latest object and retrying. +// reasonMessageForState returns a short machine-readable reason and a +// human-readable message for a connection state. The worker only observes +// tunnel up/down, so the reasons are coarse (it cannot distinguish e.g. a dial +// timeout from a not-yet-ready peer); they give operators a stable, honest +// signal without over-claiming precision. +func reasonMessageForState(state string) (reason, message string) { + switch state { + case spokev1alpha1.GatewayConnectionStateConnected: + return "TunnelEstablished", "gateway tunnel is up" + case spokev1alpha1.GatewayConnectionStateNotConnected: + return "TunnelDown", "all gateway pods report their tunnel is down" + default: // Pending / empty + return "Reconciling", "waiting for gateway tunnel connectivity to be reported" + } +} + func (r *SliceGwReconciler) reconcileGatewayConnectionStatus(ctx context.Context, sliceGw *spokev1alpha1.WorkerSliceGateway, meshSliceGw *kubeslicev1beta1.SliceGateway) error { state := deriveGatewayConnectionState(meshSliceGw.Status.GatewayPodStatus) - if sliceGw.Status.ConnectionState == state { + reason, message := reasonMessageForState(state) + // Nothing to do when neither the state nor its reason/message has drifted. + if sliceGw.Status.ConnectionState == state && sliceGw.Status.Reason == reason && sliceGw.Status.Message == message { return nil } return retry.RetryOnConflict(retry.DefaultRetry, func() error { @@ -70,12 +88,18 @@ func (r *SliceGwReconciler) reconcileGatewayConnectionStatus(ctx context.Context if err := r.Get(ctx, client.ObjectKey{Name: sliceGw.Name, Namespace: sliceGw.Namespace}, latest); err != nil { return err } - if latest.Status.ConnectionState == state { + if latest.Status.ConnectionState == state && latest.Status.Reason == reason && latest.Status.Message == message { return nil } - now := metav1.Now() + // LastTransitionTime marks connection-state changes; don't churn it on a + // reason/message-only correction. + if latest.Status.ConnectionState != state { + now := metav1.Now() + latest.Status.LastTransitionTime = &now + } latest.Status.ConnectionState = state - latest.Status.LastTransitionTime = &now + latest.Status.Reason = reason + latest.Status.Message = message return r.Status().Update(ctx, latest) }) } diff --git a/pkg/hub/controllers/slicegateway_status_test.go b/pkg/hub/controllers/slicegateway_status_test.go index a88cae8d5..9073ed3a4 100644 --- a/pkg/hub/controllers/slicegateway_status_test.go +++ b/pkg/hub/controllers/slicegateway_status_test.go @@ -138,3 +138,18 @@ func TestReconcileGatewayConnectionStatus(t *testing.T) { } }) } + +func TestReasonMessageForState(t *testing.T) { + cases := map[string]struct{ reason, msg string }{ + spokev1alpha1.GatewayConnectionStateConnected: {"TunnelEstablished", "gateway tunnel is up"}, + spokev1alpha1.GatewayConnectionStateNotConnected: {"TunnelDown", "all gateway pods report their tunnel is down"}, + spokev1alpha1.GatewayConnectionStatePending: {"Reconciling", "waiting for gateway tunnel connectivity to be reported"}, + "": {"Reconciling", "waiting for gateway tunnel connectivity to be reported"}, + } + for state, want := range cases { + r, m := reasonMessageForState(state) + if r != want.reason || m != want.msg { + t.Errorf("state %q: got (%q,%q), want (%q,%q)", state, r, m, want.reason, want.msg) + } + } +}