Skip to content

Commit e6a161c

Browse files
authored
Move GetWorkload/ListWorkload to status manager interface (#1222)
This moves the Get/List workload operations to the status manager interface. This is needed because the status manager implementation dictates which workloads can be found, and what their statuses should be. The runtimeStatusManager implementation now relies on the runtime to provide details of which workloads are available, and their state. The fileStatusManager uses the status files, but also delegates to the runtime in order to handle workloads created before the status files were created. Unit tests are added for workloads/status.go Note that this does not wire in the file-based status tracking. That will be introduced in the next PR.
1 parent 981a43a commit e6a161c

File tree

7 files changed

+1117
-159
lines changed

7 files changed

+1117
-159
lines changed

pkg/container/runtime/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
)
3939

4040
// ContainerInfo represents information about a container
41+
// TODO: Consider merging this with workloads.Workload
4142
type ContainerInfo struct {
4243
// Name is the container name
4344
Name string
@@ -56,6 +57,11 @@ type ContainerInfo struct {
5657
Ports []PortMapping
5758
}
5859

60+
// IsRunning returns true if the container is currently running.
61+
func (c *ContainerInfo) IsRunning() bool {
62+
return c.State == WorkloadStatusRunning
63+
}
64+
5965
// PortMapping represents a port mapping for a container
6066
type PortMapping struct {
6167
// ContainerPort is the port inside the container

pkg/workloads/file_status.go

Lines changed: 171 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/adrg/xdg"
1313
"github.com/gofrs/flock"
1414

15-
"github.com/stacklok/toolhive/pkg/container/runtime"
15+
rt "github.com/stacklok/toolhive/pkg/container/runtime"
1616
"github.com/stacklok/toolhive/pkg/logger"
1717
)
1818

@@ -27,7 +27,7 @@ const (
2727

2828
// NewFileStatusManager creates a new file-based StatusManager.
2929
// Status files will be stored in the XDG data directory under "statuses/".
30-
func NewFileStatusManager() StatusManager {
30+
func NewFileStatusManager(runtime rt.Runtime) StatusManager {
3131
// Get the base directory using XDG data directory
3232
baseDir, err := xdg.DataFile(statusesPrefix)
3333
if err != nil {
@@ -39,6 +39,7 @@ func NewFileStatusManager() StatusManager {
3939

4040
return &fileStatusManager{
4141
baseDir: baseDir,
42+
runtime: runtime,
4243
}
4344
}
4445

@@ -47,14 +48,15 @@ func NewFileStatusManager() StatusManager {
4748
// to prevent concurrent access issues.
4849
type fileStatusManager struct {
4950
baseDir string
51+
runtime rt.Runtime
5052
}
5153

5254
// workloadStatusFile represents the JSON structure stored on disk
5355
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"`
5860
}
5961

6062
// CreateWorkloadStatus creates the initial `starting` status for a new workload.
@@ -71,7 +73,7 @@ func (f *fileStatusManager) CreateWorkloadStatus(ctx context.Context, workloadNa
7173
// Create initial status
7274
now := time.Now()
7375
statusFile := workloadStatusFile{
74-
Status: runtime.WorkloadStatusStarting,
76+
Status: rt.WorkloadStatusStarting,
7577
StatusContext: "",
7678
CreatedAt: now,
7779
UpdatedAt: now,
@@ -86,15 +88,16 @@ func (f *fileStatusManager) CreateWorkloadStatus(ctx context.Context, workloadNa
8688
})
8789
}
8890

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
9395

9496
err := f.withFileReadLock(ctx, workloadName, func(statusFilePath string) error {
9597
// Check if file exists
9698
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
98101
} else if err != nil {
99102
return fmt.Errorf("failed to check status file for workload %s: %w", workloadName, err)
100103
}
@@ -104,18 +107,121 @@ func (f *fileStatusManager) GetWorkloadStatus(ctx context.Context, workloadName
104107
return fmt.Errorf("failed to read status for workload %s: %w", workloadName, err)
105108
}
106109

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
109114
return nil
110115
})
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+
}
111137

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
113219
}
114220

115221
// SetWorkloadStatus sets the status of a workload by its name.
116222
// This method will do nothing if the workload does not exist, following the interface contract.
117223
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,
119225
) {
120226
err := f.withFileLock(ctx, workloadName, func(statusFilePath string) error {
121227
// Check if file exists
@@ -278,3 +384,52 @@ func (*fileStatusManager) writeStatusFile(statusFilePath string, statusFile work
278384

279385
return nil
280386
}
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

Comments
 (0)