@@ -12,7 +12,7 @@ import (
12
12
"github.com/adrg/xdg"
13
13
"github.com/gofrs/flock"
14
14
15
- "github.com/stacklok/toolhive/pkg/container/runtime"
15
+ rt "github.com/stacklok/toolhive/pkg/container/runtime"
16
16
"github.com/stacklok/toolhive/pkg/logger"
17
17
)
18
18
@@ -27,7 +27,7 @@ const (
27
27
28
28
// NewFileStatusManager creates a new file-based StatusManager.
29
29
// Status files will be stored in the XDG data directory under "statuses/".
30
- func NewFileStatusManager () StatusManager {
30
+ func NewFileStatusManager (runtime rt. Runtime ) StatusManager {
31
31
// Get the base directory using XDG data directory
32
32
baseDir , err := xdg .DataFile (statusesPrefix )
33
33
if err != nil {
@@ -39,6 +39,7 @@ func NewFileStatusManager() StatusManager {
39
39
40
40
return & fileStatusManager {
41
41
baseDir : baseDir ,
42
+ runtime : runtime ,
42
43
}
43
44
}
44
45
@@ -47,14 +48,15 @@ func NewFileStatusManager() StatusManager {
47
48
// to prevent concurrent access issues.
48
49
type fileStatusManager struct {
49
50
baseDir string
51
+ runtime rt.Runtime
50
52
}
51
53
52
54
// workloadStatusFile represents the JSON structure stored on disk
53
55
type workloadStatusFile struct {
54
- Status runtime .WorkloadStatus `json:"status"`
55
- StatusContext string `json:"status_context,omitempty"`
56
- CreatedAt time.Time `json:"created_at"`
57
- UpdatedAt time.Time `json:"updated_at"`
56
+ Status rt .WorkloadStatus `json:"status"`
57
+ StatusContext string `json:"status_context,omitempty"`
58
+ CreatedAt time.Time `json:"created_at"`
59
+ UpdatedAt time.Time `json:"updated_at"`
58
60
}
59
61
60
62
// CreateWorkloadStatus creates the initial `starting` status for a new workload.
@@ -71,7 +73,7 @@ func (f *fileStatusManager) CreateWorkloadStatus(ctx context.Context, workloadNa
71
73
// Create initial status
72
74
now := time .Now ()
73
75
statusFile := workloadStatusFile {
74
- Status : runtime .WorkloadStatusStarting ,
76
+ Status : rt .WorkloadStatusStarting ,
75
77
StatusContext : "" ,
76
78
CreatedAt : now ,
77
79
UpdatedAt : now ,
@@ -86,15 +88,16 @@ func (f *fileStatusManager) CreateWorkloadStatus(ctx context.Context, workloadNa
86
88
})
87
89
}
88
90
89
- // GetWorkloadStatus retrieves the status of a workload by its name.
90
- func (f * fileStatusManager ) GetWorkloadStatus (ctx context.Context , workloadName string ) (runtime. WorkloadStatus , string , error ) {
91
- result := runtime . WorkloadStatusUnknown
92
- var statusContext string
91
+ // GetWorkload retrieves the status of a workload by its name.
92
+ func (f * fileStatusManager ) GetWorkload (ctx context.Context , workloadName string ) (Workload , error ) {
93
+ result := Workload { Name : workloadName }
94
+ fileFound := false
93
95
94
96
err := f .withFileReadLock (ctx , workloadName , func (statusFilePath string ) error {
95
97
// Check if file exists
96
98
if _ , err := os .Stat (statusFilePath ); os .IsNotExist (err ) {
97
- return fmt .Errorf ("workload %s not found" , workloadName )
99
+ // File doesn't exist, we'll fall back to runtime check
100
+ return nil
98
101
} else if err != nil {
99
102
return fmt .Errorf ("failed to check status file for workload %s: %w" , workloadName , err )
100
103
}
@@ -104,18 +107,121 @@ func (f *fileStatusManager) GetWorkloadStatus(ctx context.Context, workloadName
104
107
return fmt .Errorf ("failed to read status for workload %s: %w" , workloadName , err )
105
108
}
106
109
107
- result = statusFile .Status
108
- statusContext = statusFile .StatusContext
110
+ result .Status = statusFile .Status
111
+ result .StatusContext = statusFile .StatusContext
112
+ result .CreatedAt = statusFile .CreatedAt
113
+ fileFound = true
109
114
return nil
110
115
})
116
+ if err != nil {
117
+ return Workload {}, err
118
+ }
119
+
120
+ // If file was found and workload is running, get additional info from runtime
121
+ if fileFound && result .Status == rt .WorkloadStatusRunning {
122
+ // TODO: Find discrepancies between the file and runtime workload.
123
+ runtimeResult , err := f .getWorkloadFromRuntime (ctx , workloadName )
124
+ if err != nil {
125
+ return Workload {}, err
126
+ }
127
+ // Use runtime data but preserve file-based status info
128
+ fileStatus := result .Status
129
+ fileStatusContext := result .StatusContext
130
+ fileCreatedAt := result .CreatedAt
131
+ result = runtimeResult
132
+ result .Status = fileStatus // Keep the file status
133
+ result .StatusContext = fileStatusContext // Keep the file status context
134
+ result .CreatedAt = fileCreatedAt // Keep the file created time
135
+ return result , nil
136
+ }
111
137
112
- return result , statusContext , err
138
+ // If file was found and workload is not running, return file data
139
+ if fileFound {
140
+ return result , nil
141
+ }
142
+
143
+ // File not found, fall back to runtime check
144
+ return f .getWorkloadFromRuntime (ctx , workloadName )
145
+ }
146
+
147
+ func (f * fileStatusManager ) ListWorkloads (ctx context.Context , listAll bool , labelFilters []string ) ([]Workload , error ) {
148
+ // Parse the filters into a format we can use for matching.
149
+ parsedFilters , err := parseLabelFilters (labelFilters )
150
+ if err != nil {
151
+ return nil , fmt .Errorf ("failed to parse label filters: %v" , err )
152
+ }
153
+
154
+ // Get workloads from runtime
155
+ runtimeContainers , err := f .runtime .ListWorkloads (ctx )
156
+ if err != nil {
157
+ return nil , fmt .Errorf ("failed to list workloads from runtime: %w" , err )
158
+ }
159
+
160
+ // Get workloads from files
161
+ fileWorkloads , err := f .getWorkloadsFromFiles ()
162
+ if err != nil {
163
+ return nil , fmt .Errorf ("failed to get workloads from files: %w" , err )
164
+ }
165
+
166
+ // Create a map of runtime workloads by name for easy lookup
167
+ runtimeWorkloadMap := make (map [string ]rt.ContainerInfo )
168
+ for _ , container := range runtimeContainers {
169
+ runtimeWorkloadMap [container .Name ] = container
170
+ }
171
+
172
+ // Create result map to avoid duplicates and merge data
173
+ workloadMap := make (map [string ]Workload )
174
+
175
+ // First, add all runtime workloads
176
+ for _ , container := range runtimeContainers {
177
+ workload , err := WorkloadFromContainerInfo (& container )
178
+ if err != nil {
179
+ logger .Warnf ("failed to convert container info for workload %s: %v" , container .Name , err )
180
+ continue
181
+ }
182
+ workloadMap [container .Name ] = workload
183
+ }
184
+
185
+ // Then, merge with file workloads, preferring file status
186
+ for name , fileWorkload := range fileWorkloads {
187
+ if runtimeWorkload , exists := workloadMap [name ]; exists {
188
+ // Merge: use runtime data but prefer file status
189
+ merged := runtimeWorkload
190
+ merged .Status = fileWorkload .Status
191
+ merged .StatusContext = fileWorkload .StatusContext
192
+ merged .CreatedAt = fileWorkload .CreatedAt
193
+ workloadMap [name ] = merged
194
+ } else {
195
+ // File-only workload (runtime not available)
196
+ workloadMap [name ] = fileWorkload
197
+ }
198
+ }
199
+
200
+ // Convert map to slice and apply filters
201
+ var workloads []Workload
202
+ for _ , workload := range workloadMap {
203
+ // Apply listAll filter
204
+ if ! listAll && workload .Status != rt .WorkloadStatusRunning {
205
+ continue
206
+ }
207
+
208
+ // Apply label filters
209
+ if len (parsedFilters ) > 0 {
210
+ if ! matchesLabelFilters (workload .Labels , parsedFilters ) {
211
+ continue
212
+ }
213
+ }
214
+
215
+ workloads = append (workloads , workload )
216
+ }
217
+
218
+ return workloads , nil
113
219
}
114
220
115
221
// SetWorkloadStatus sets the status of a workload by its name.
116
222
// This method will do nothing if the workload does not exist, following the interface contract.
117
223
func (f * fileStatusManager ) SetWorkloadStatus (
118
- ctx context.Context , workloadName string , status runtime .WorkloadStatus , contextMsg string ,
224
+ ctx context.Context , workloadName string , status rt .WorkloadStatus , contextMsg string ,
119
225
) {
120
226
err := f .withFileLock (ctx , workloadName , func (statusFilePath string ) error {
121
227
// Check if file exists
@@ -278,3 +384,52 @@ func (*fileStatusManager) writeStatusFile(statusFilePath string, statusFile work
278
384
279
385
return nil
280
386
}
387
+
388
+ // getWorkloadFromRuntime retrieves workload information from the runtime.
389
+ func (f * fileStatusManager ) getWorkloadFromRuntime (ctx context.Context , workloadName string ) (Workload , error ) {
390
+ info , err := f .runtime .GetWorkloadInfo (ctx , workloadName )
391
+ if err != nil {
392
+ return Workload {}, fmt .Errorf ("failed to get workload info from runtime: %w" , err )
393
+ }
394
+
395
+ return WorkloadFromContainerInfo (& info )
396
+ }
397
+
398
+ // getWorkloadsFromFiles retrieves all workloads from status files.
399
+ func (f * fileStatusManager ) getWorkloadsFromFiles () (map [string ]Workload , error ) {
400
+ // Ensure base directory exists
401
+ if err := f .ensureBaseDir (); err != nil {
402
+ return nil , fmt .Errorf ("failed to ensure base directory: %w" , err )
403
+ }
404
+
405
+ // List all .json files in the base directory
406
+ files , err := filepath .Glob (filepath .Join (f .baseDir , "*.json" ))
407
+ if err != nil {
408
+ return nil , fmt .Errorf ("failed to list status files: %w" , err )
409
+ }
410
+
411
+ workloads := make (map [string ]Workload )
412
+ for _ , file := range files {
413
+ // Extract workload name from filename (remove .json extension)
414
+ workloadName := strings .TrimSuffix (filepath .Base (file ), ".json" )
415
+
416
+ // Read the status file
417
+ statusFile , err := f .readStatusFile (file )
418
+ if err != nil {
419
+ logger .Warnf ("failed to read status file %s: %v" , file , err )
420
+ continue
421
+ }
422
+
423
+ // Create workload from file data
424
+ workload := Workload {
425
+ Name : workloadName ,
426
+ Status : statusFile .Status ,
427
+ StatusContext : statusFile .StatusContext ,
428
+ CreatedAt : statusFile .CreatedAt ,
429
+ }
430
+
431
+ workloads [workloadName ] = workload
432
+ }
433
+
434
+ return workloads , nil
435
+ }
0 commit comments