Skip to content

Commit 2b32116

Browse files
authored
Also implement consistency checks in ListWorkloads (#1381)
This was meant to be included in my previous PR.
1 parent 65e5aff commit 2b32116

File tree

2 files changed

+146
-8
lines changed

2 files changed

+146
-8
lines changed

pkg/workloads/statuses/file_status.go

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,22 @@ func (f *fileStatusManager) ListWorkloads(ctx context.Context, listAll bool, lab
146146
workloadMap[container.Name] = workload
147147
}
148148

149-
// Then, merge with file workloads, preferring file status
149+
// Then, merge with file workloads, validating running workloads
150150
for name, fileWorkload := range fileWorkloads {
151-
if runtimeWorkload, exists := workloadMap[name]; exists {
152-
// Merge: use runtime data but prefer file status
153-
merged := runtimeWorkload
154-
merged.Status = fileWorkload.Status
155-
merged.StatusContext = fileWorkload.StatusContext
156-
merged.CreatedAt = fileWorkload.CreatedAt
157-
workloadMap[name] = merged
151+
if runtimeContainer, exists := runtimeWorkloadMap[name]; exists {
152+
// Validate running workloads similar to GetWorkload
153+
validatedWorkload, err := f.validateWorkloadInList(ctx, name, fileWorkload, runtimeContainer)
154+
if err != nil {
155+
logger.Warnf("failed to validate workload %s in list: %v", name, err)
156+
// Fall back to basic merge without validation
157+
runtimeWorkload := workloadMap[name]
158+
runtimeWorkload.Status = fileWorkload.Status
159+
runtimeWorkload.StatusContext = fileWorkload.StatusContext
160+
runtimeWorkload.CreatedAt = fileWorkload.CreatedAt
161+
workloadMap[name] = runtimeWorkload
162+
} else {
163+
workloadMap[name] = validatedWorkload
164+
}
158165
} else {
159166
// File-only workload (runtime not available)
160167
workloadMap[name] = fileWorkload
@@ -506,3 +513,36 @@ func (*fileStatusManager) mergeHealthyWorkloadData(containerInfo rt.ContainerInf
506513
runtimeResult.CreatedAt = result.CreatedAt // Keep the file created time
507514
return runtimeResult, nil
508515
}
516+
517+
// validateWorkloadInList validates a workload during list operations, similar to validateRunningWorkload
518+
// but with different error handling to avoid disrupting the entire list operation.
519+
func (f *fileStatusManager) validateWorkloadInList(
520+
ctx context.Context, workloadName string, fileWorkload core.Workload, containerInfo rt.ContainerInfo,
521+
) (core.Workload, error) {
522+
// Only validate if file shows running status
523+
if fileWorkload.Status != rt.WorkloadStatusRunning {
524+
// For non-running workloads, just merge runtime data with file status
525+
runtimeWorkload, err := types.WorkloadFromContainerInfo(&containerInfo)
526+
if err != nil {
527+
return core.Workload{}, err
528+
}
529+
runtimeWorkload.Status = fileWorkload.Status
530+
runtimeWorkload.StatusContext = fileWorkload.StatusContext
531+
runtimeWorkload.CreatedAt = fileWorkload.CreatedAt
532+
return runtimeWorkload, nil
533+
}
534+
535+
// For running workloads, apply full validation
536+
// Check if runtime status matches file status
537+
if containerInfo.State != rt.WorkloadStatusRunning {
538+
return f.handleRuntimeMismatch(ctx, workloadName, fileWorkload, containerInfo)
539+
}
540+
541+
// Check if proxy process is running when workload is running
542+
if unhealthyWorkload, isUnhealthy := f.checkProxyHealth(ctx, workloadName, fileWorkload, containerInfo); isUnhealthy {
543+
return unhealthyWorkload, nil
544+
}
545+
546+
// Runtime and proxy confirm workload is healthy - merge runtime data with file status
547+
return f.mergeHealthyWorkloadData(containerInfo, fileWorkload)
548+
}

pkg/workloads/statuses/file_status_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"os"
88
"path/filepath"
9+
"strings"
910
"testing"
1011
"time"
1112

@@ -860,3 +861,100 @@ func TestFileStatusManager_GetWorkload_HealthyWithProxy(t *testing.T) {
860861
assert.Equal(t, "container started", workload.StatusContext) // Original file context preserved
861862
assert.Equal(t, "test-image:latest", workload.Package)
862863
}
864+
865+
func TestFileStatusManager_ListWorkloads_WithValidation(t *testing.T) {
866+
t.Parallel()
867+
868+
ctrl := gomock.NewController(t)
869+
defer ctrl.Finish()
870+
871+
tempDir := t.TempDir()
872+
mockRuntime := mocks.NewMockRuntime(ctrl)
873+
manager := &fileStatusManager{
874+
baseDir: tempDir,
875+
runtime: mockRuntime,
876+
}
877+
ctx := context.Background()
878+
879+
// Create file workloads - one healthy running, one with runtime mismatch, one with proxy down
880+
err := manager.SetWorkloadStatus(ctx, "healthy-workload", rt.WorkloadStatusRunning, "container started")
881+
require.NoError(t, err)
882+
883+
err = manager.SetWorkloadStatus(ctx, "runtime-mismatch", rt.WorkloadStatusRunning, "container started")
884+
require.NoError(t, err)
885+
886+
err = manager.SetWorkloadStatus(ctx, "proxy-down", rt.WorkloadStatusRunning, "container started")
887+
require.NoError(t, err)
888+
889+
// Mock runtime containers
890+
runtimeContainers := []rt.ContainerInfo{
891+
{
892+
Name: "healthy-workload",
893+
Image: "healthy:latest",
894+
Status: "Up 5 minutes",
895+
State: rt.WorkloadStatusRunning,
896+
Labels: map[string]string{
897+
"toolhive": "true",
898+
"toolhive-name": "healthy-workload",
899+
},
900+
},
901+
{
902+
Name: "runtime-mismatch",
903+
Image: "mismatch:latest",
904+
Status: "Exited (0) 1 minute ago",
905+
State: rt.WorkloadStatusStopped, // Runtime says stopped, file says running
906+
Labels: map[string]string{
907+
"toolhive": "true",
908+
"toolhive-name": "runtime-mismatch",
909+
},
910+
},
911+
{
912+
Name: "proxy-down",
913+
Image: "proxy:latest",
914+
Status: "Up 3 minutes",
915+
State: rt.WorkloadStatusRunning,
916+
Labels: map[string]string{
917+
"toolhive": "true",
918+
"toolhive-name": "proxy-down",
919+
"toolhive-basename": "proxy-down", // This will trigger proxy check
920+
},
921+
},
922+
}
923+
924+
mockRuntime.EXPECT().ListWorkloads(gomock.Any()).Return(runtimeContainers, nil)
925+
926+
// List all workloads
927+
workloads, err := manager.ListWorkloads(ctx, true, nil)
928+
require.NoError(t, err)
929+
930+
// Should have 3 workloads
931+
require.Len(t, workloads, 3)
932+
933+
// Create a map for easier assertion
934+
workloadMap := make(map[string]core.Workload)
935+
for _, w := range workloads {
936+
workloadMap[w.Name] = w
937+
}
938+
939+
// Verify healthy workload remains running
940+
healthyWorkload, exists := workloadMap["healthy-workload"]
941+
require.True(t, exists)
942+
assert.Equal(t, rt.WorkloadStatusRunning, healthyWorkload.Status)
943+
944+
// Verify runtime mismatch workload is marked unhealthy (status might be updated async)
945+
// We'll check for either unhealthy or the original status with mismatch context
946+
runtimeMismatch, exists := workloadMap["runtime-mismatch"]
947+
require.True(t, exists)
948+
// The workload should either be marked unhealthy or have a status context indicating the issue
949+
isValidatedUnhealthy := runtimeMismatch.Status == rt.WorkloadStatusUnhealthy ||
950+
strings.Contains(runtimeMismatch.StatusContext, "mismatch")
951+
assert.True(t, isValidatedUnhealthy, "Runtime mismatch workload should be detected as unhealthy")
952+
953+
// Verify proxy down workload is detected (proxy.IsRunning will return false for non-existent proxy)
954+
proxyDown, exists := workloadMap["proxy-down"]
955+
require.True(t, exists)
956+
// Similar check - should be unhealthy or have proxy-related context
957+
isProxyValidated := proxyDown.Status == rt.WorkloadStatusUnhealthy ||
958+
strings.Contains(proxyDown.StatusContext, "proxy")
959+
assert.True(t, isProxyValidated, "Proxy down workload should be detected as unhealthy")
960+
}

0 commit comments

Comments
 (0)