diff --git a/cluster/kube/client.go b/cluster/kube/client.go index 340641882..6f2470744 100644 --- a/cluster/kube/client.go +++ b/cluster/kube/client.go @@ -935,13 +935,15 @@ func (c *client) ServiceStatus(ctx context.Context, lid mtypes.LeaseID, name str return nil, kubeclienterrors.ErrNoServiceForLease } + // Check if this service uses persistent storage (should be StatefulSet) + // This logic must match the deployment creation logic in Deploy() isDeployment := true - if params := svc.Params; params != nil { - for _, param := range params.Storage { - if param.Mount != "" { - isDeployment = false - break - } + persistent := false + for i := range svc.Resources.Storage { + attrVal := svc.Resources.Storage[i].Attributes.Find(sdl.StorageAttributePersistent) + if persistent, _ = attrVal.AsBool(); persistent { + isDeployment = false + break } } diff --git a/cluster/kube/client_test.go b/cluster/kube/client_test.go index f0b7f1b7a..05922bd2e 100644 --- a/cluster/kube/client_test.go +++ b/cluster/kube/client_test.go @@ -7,6 +7,7 @@ import ( manifest "github.com/akash-network/akash-api/go/manifest/v2beta2" mtypes "github.com/akash-network/akash-api/go/node/market/v1beta4" types "github.com/akash-network/akash-api/go/node/types/v1beta3" + "github.com/akash-network/node/sdl" "github.com/akash-network/node/testutil" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" @@ -687,3 +688,153 @@ func TestServiceStatusWithoutIngress(t *testing.T) { require.NotNil(t, status) require.Len(t, status.URIs, 0) } + +func TestServiceStatusStatefulSetDetection(t *testing.T) { + lid := testutil.LeaseID(t) + ns := builder.LidNS(lid) + + lns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: ns, + }, + } + + testCases := []struct { + name string + serviceName string + services manifest.Services + expectedWorkload string + }{ + { + name: "PersistentStorage_UsesStatefulSet", + serviceName: "postgres", + services: manifest.Services{ + { + Name: "postgres", + Image: "postgres:latest", + Resources: types.Resources{ + Storage: []types.Storage{ + { + Name: "data", + Quantity: types.NewResourceValue(10737418240), // 10Gi + Attributes: types.Attributes{ + { + Key: sdl.StorageAttributePersistent, + Value: "true", + }, + }, + }, + }, + }, + Count: 1, + }, + }, + expectedWorkload: "StatefulSet", + }, + { + name: "NonPersistentStorage_UsesDeployment", + serviceName: "web", + services: manifest.Services{ + { + Name: "web", + Image: "nginx:latest", + Resources: types.Resources{ + Storage: []types.Storage{ + { + Name: "tmp", + Quantity: types.NewResourceValue(1073741824), // 1Gi + Attributes: types.Attributes{ + { + Key: sdl.StorageAttributePersistent, + Value: "false", + }, + }, + }, + }, + }, + Count: 2, + }, + }, + expectedWorkload: "Deployment", + }, + { + name: "NoStorage_UsesDeployment", + serviceName: "api", + services: manifest.Services{ + { + Name: "api", + Image: "myapp:latest", + Resources: types.Resources{}, + Count: 3, + }, + }, + expectedWorkload: "Deployment", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create the manifest + mg := &manifest.Group{ + Name: "test-group", + Services: tc.services, + } + + cparams := crd.ClusterSettings{ + SchedulerParams: make([]*crd.SchedulerParams, len(mg.Services)), + } + + m, err := crd.NewManifest(testKubeClientNs, lid, mg, cparams) + require.NoError(t, err) + + // Create the deployment or statefulset + var kobjs []runtime.Object + kobjs = append(kobjs, lns) + + if tc.expectedWorkload == "StatefulSet" { + ss := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.serviceName, + Namespace: ns, + }, + Spec: appsv1.StatefulSetSpec{}, + Status: appsv1.StatefulSetStatus{ + AvailableReplicas: 1, + Replicas: 1, + }, + } + kobjs = append(kobjs, ss) + } else { + depl := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.serviceName, + Namespace: ns, + }, + Spec: appsv1.DeploymentSpec{}, + Status: appsv1.DeploymentStatus{ + AvailableReplicas: int32(tc.services[0].Count), + Replicas: int32(tc.services[0].Count), + }, + } + kobjs = append(kobjs, depl) + } + + clientInterface := clientForTest(t, kobjs, []runtime.Object{m}) + + // Test ServiceStatus + status, err := clientInterface.ServiceStatus(context.Background(), lid, tc.serviceName) + require.NoError(t, err) + require.NotNil(t, status) + require.Equal(t, tc.serviceName, status.Name) + + // Verify we can find the correct workload type + if tc.expectedWorkload == "StatefulSet" { + require.Equal(t, uint32(1), status.Available) + require.Equal(t, uint32(1), status.Total) + } else { + require.Equal(t, tc.services[0].Count, status.Available) + require.Equal(t, tc.services[0].Count, status.Total) + } + }) + } +} diff --git a/gateway/rest/router.go b/gateway/rest/router.go index 3a244e818..8b4c89845 100644 --- a/gateway/rest/router.go +++ b/gateway/rest/router.go @@ -372,7 +372,9 @@ func leaseShellHandler(log log.Logger, cclient cluster.Client) http.HandlerFunc if cluster.ErrorIsOkToSendToClient(err) || errors.Is(err, kubeclienterrors.ErrNoServiceForLease) { responseData.Message = err.Error() } else { - http.Error(rw, err.Error(), http.StatusInternalServerError) + resultWriter = wsutil.NewWsWriterWrapper(shellWs, LeaseShellCodeFailure, l) + encodeData = false + localLog.Error("service status check failed", "err", err) } }