Skip to content

Commit 9179827

Browse files
authored
Use one workload status type consistently (#1213)
Prior to this change, the Docker runtime returned Docker status values, the kubernetes code manufactured something which looked like Docker statuses, and the workload manager attempted to convert them to a single consistent enum type. This code changes the runtime layer to return instances of our WorkloadStatus enum, and removes the translation from the workload manager.
1 parent c9fd2e7 commit 9179827

File tree

11 files changed

+116
-103
lines changed

11 files changed

+116
-103
lines changed

docs/server/docs.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/server/swagger.yaml

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/container/docker/client.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ func (c *Client) ListWorkloads(ctx context.Context) ([]runtime.ContainerInfo, er
252252
Name: name,
253253
Image: c.Image,
254254
Status: c.Status,
255-
State: c.State,
255+
State: dockerToDomainStatus(c.State),
256256
Created: created,
257257
Labels: c.Labels,
258258
Ports: ports,
@@ -276,7 +276,7 @@ func (c *Client) StopWorkload(ctx context.Context, workloadID string) error {
276276
}
277277

278278
// If the container is not running, return success
279-
if info.State != container.StateRunning {
279+
if info.State != runtime.WorkloadStatusRunning {
280280
return nil
281281
}
282282

@@ -441,7 +441,7 @@ func (c *Client) GetWorkloadInfo(ctx context.Context, workloadID string) (runtim
441441
Name: strings.TrimPrefix(info.Name, "/"),
442442
Image: info.Config.Image,
443443
Status: info.State.Status,
444-
State: info.State.Status,
444+
State: dockerToDomainStatus(info.State.Status),
445445
Created: created,
446446
Labels: info.Config.Labels,
447447
Ports: ports,
@@ -1487,3 +1487,19 @@ func (c *Client) deleteNetworks(ctx context.Context, containerName string) error
14871487
}
14881488
return nil
14891489
}
1490+
1491+
func dockerToDomainStatus(status string) runtime.WorkloadStatus {
1492+
// Reference: https://docs.docker.com/reference/cli/docker/container/ls/#status
1493+
switch status {
1494+
case "running":
1495+
return runtime.WorkloadStatusRunning
1496+
case "created", "restarting":
1497+
return runtime.WorkloadStatusStarting
1498+
case "paused", "exited", "dead":
1499+
return runtime.WorkloadStatusStopped
1500+
case "removing": // TODO: add handling new workload creation
1501+
return runtime.WorkloadStatusRemoving
1502+
}
1503+
// We should not reach here.
1504+
return runtime.WorkloadStatusUnknown
1505+
}

pkg/container/kubernetes/client.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -343,16 +343,18 @@ func (c *Client) GetWorkloadInfo(ctx context.Context, workloadID string) (runtim
343343
}
344344

345345
// Determine status and state
346-
var status, state string
346+
var status string
347+
var state runtime.WorkloadStatus
347348
if statefulset.Status.ReadyReplicas > 0 {
348349
status = "Running"
349-
state = "running"
350+
state = runtime.WorkloadStatusRunning
350351
} else if statefulset.Status.Replicas > 0 {
351352
status = "Pending"
352-
state = "pending"
353+
state = runtime.WorkloadStatusStarting
353354
} else {
355+
// NOTE: Not clear if this is correct since the stop operation is a no-op.
354356
status = "Stopped"
355-
state = "stopped"
357+
state = runtime.WorkloadStatusStopped
356358
}
357359

358360
// Get the image from the pod template
@@ -420,17 +422,17 @@ func (c *Client) ListWorkloads(ctx context.Context) ([]runtime.ContainerInfo, er
420422

421423
// Get container status
422424
status := UnknownStatus
423-
state := UnknownStatus
425+
state := runtime.WorkloadStatusUnknown
424426
if len(pod.Status.ContainerStatuses) > 0 {
425427
containerStatus := pod.Status.ContainerStatuses[0]
426428
if containerStatus.State.Running != nil {
427-
state = "running"
429+
state = runtime.WorkloadStatusRunning
428430
status = "Running"
429431
} else if containerStatus.State.Waiting != nil {
430-
state = "waiting"
432+
state = runtime.WorkloadStatusStarting
431433
status = containerStatus.State.Waiting.Reason
432434
} else if containerStatus.State.Terminated != nil {
433-
state = "terminated"
435+
state = runtime.WorkloadStatusRemoving
434436
status = containerStatus.State.Terminated.Reason
435437
}
436438
}

pkg/container/runtime/types.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,30 @@ import (
1212
"github.com/stacklok/toolhive/pkg/permissions"
1313
)
1414

15+
// WorkloadStatus is an enum representing the possible statuses of a workload.
16+
type WorkloadStatus string
17+
18+
const (
19+
// WorkloadStatusRunning indicates that the workload is currently running.
20+
WorkloadStatusRunning WorkloadStatus = "running"
21+
// WorkloadStatusStopped indicates that the workload is stopped.
22+
WorkloadStatusStopped WorkloadStatus = "stopped"
23+
// WorkloadStatusError indicates that the workload has encountered an error
24+
// during creation/stop/restart/delete.
25+
WorkloadStatusError WorkloadStatus = "error"
26+
// WorkloadStatusStarting indicates that the workload is being started.
27+
WorkloadStatusStarting WorkloadStatus = "starting"
28+
// WorkloadStatusStopping indicates that the workload is being stopped.
29+
WorkloadStatusStopping WorkloadStatus = "stopping"
30+
// WorkloadStatusUnhealthy indicates that the workload is running, but is
31+
// in an inconsistent state which prevents normal operation.
32+
WorkloadStatusUnhealthy WorkloadStatus = "unhealthy"
33+
// WorkloadStatusRemoving indicates that the workload is being removed.
34+
WorkloadStatusRemoving WorkloadStatus = "removing"
35+
// WorkloadStatusUnknown indicates that the workload status is unknown.
36+
WorkloadStatusUnknown WorkloadStatus = "unknown"
37+
)
38+
1539
// ContainerInfo represents information about a container
1640
type ContainerInfo struct {
1741
// ID is the container ID
@@ -21,9 +45,10 @@ type ContainerInfo struct {
2145
// Image is the container image
2246
Image string
2347
// Status is the container status
48+
// This is usually some human-readable context.
2449
Status string
2550
// State is the container state
26-
State string
51+
State WorkloadStatus
2752
// Created is the container creation timestamp
2853
Created time.Time
2954
// Labels is the container labels

pkg/workloads/file_status.go

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

15+
"github.com/stacklok/toolhive/pkg/container/runtime"
1516
"github.com/stacklok/toolhive/pkg/logger"
1617
)
1718

@@ -50,10 +51,10 @@ type fileStatusManager struct {
5051

5152
// workloadStatusFile represents the JSON structure stored on disk
5253
type workloadStatusFile struct {
53-
Status WorkloadStatus `json:"status"`
54-
StatusContext string `json:"status_context,omitempty"`
55-
CreatedAt time.Time `json:"created_at"`
56-
UpdatedAt time.Time `json:"updated_at"`
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"`
5758
}
5859

5960
// CreateWorkloadStatus creates the initial `starting` status for a new workload.
@@ -70,7 +71,7 @@ func (f *fileStatusManager) CreateWorkloadStatus(ctx context.Context, workloadNa
7071
// Create initial status
7172
now := time.Now()
7273
statusFile := workloadStatusFile{
73-
Status: WorkloadStatusStarting,
74+
Status: runtime.WorkloadStatusStarting,
7475
StatusContext: "",
7576
CreatedAt: now,
7677
UpdatedAt: now,
@@ -86,8 +87,8 @@ func (f *fileStatusManager) CreateWorkloadStatus(ctx context.Context, workloadNa
8687
}
8788

8889
// GetWorkloadStatus retrieves the status of a workload by its name.
89-
func (f *fileStatusManager) GetWorkloadStatus(ctx context.Context, workloadName string) (*WorkloadStatus, string, error) {
90-
var result *WorkloadStatus
90+
func (f *fileStatusManager) GetWorkloadStatus(ctx context.Context, workloadName string) (runtime.WorkloadStatus, string, error) {
91+
result := runtime.WorkloadStatusUnknown
9192
var statusContext string
9293

9394
err := f.withFileReadLock(ctx, workloadName, func(statusFilePath string) error {
@@ -103,7 +104,7 @@ func (f *fileStatusManager) GetWorkloadStatus(ctx context.Context, workloadName
103104
return fmt.Errorf("failed to read status for workload %s: %w", workloadName, err)
104105
}
105106

106-
result = &statusFile.Status
107+
result = statusFile.Status
107108
statusContext = statusFile.StatusContext
108109
return nil
109110
})
@@ -114,7 +115,7 @@ func (f *fileStatusManager) GetWorkloadStatus(ctx context.Context, workloadName
114115
// SetWorkloadStatus sets the status of a workload by its name.
115116
// This method will do nothing if the workload does not exist, following the interface contract.
116117
func (f *fileStatusManager) SetWorkloadStatus(
117-
ctx context.Context, workloadName string, status WorkloadStatus, contextMsg string,
118+
ctx context.Context, workloadName string, status runtime.WorkloadStatus, contextMsg string,
118119
) {
119120
err := f.withFileLock(ctx, workloadName, func(statusFilePath string) error {
120121
// Check if file exists

pkg/workloads/file_status_test.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
1313

14+
"github.com/stacklok/toolhive/pkg/container/runtime"
1415
"github.com/stacklok/toolhive/pkg/logger"
1516
)
1617

@@ -42,7 +43,7 @@ func TestFileStatusManager_CreateWorkloadStatus(t *testing.T) {
4243
err = json.Unmarshal(data, &statusFileData)
4344
require.NoError(t, err)
4445

45-
assert.Equal(t, WorkloadStatusStarting, statusFileData.Status)
46+
assert.Equal(t, runtime.WorkloadStatusStarting, statusFileData.Status)
4647
assert.Empty(t, statusFileData.StatusContext)
4748
assert.False(t, statusFileData.CreatedAt.IsZero())
4849
assert.False(t, statusFileData.UpdatedAt.IsZero())
@@ -77,7 +78,7 @@ func TestFileStatusManager_GetWorkloadStatus(t *testing.T) {
7778
// Get the status
7879
status, statusContext, err := manager.GetWorkloadStatus(ctx, "test-workload")
7980
require.NoError(t, err)
80-
assert.Equal(t, WorkloadStatusStarting, *status)
81+
assert.Equal(t, runtime.WorkloadStatusStarting, status)
8182
assert.Empty(t, statusContext)
8283
}
8384

@@ -104,12 +105,12 @@ func TestFileStatusManager_SetWorkloadStatus(t *testing.T) {
104105
require.NoError(t, err)
105106

106107
// Update the status
107-
manager.SetWorkloadStatus(ctx, "test-workload", WorkloadStatusRunning, "container started")
108+
manager.SetWorkloadStatus(ctx, "test-workload", runtime.WorkloadStatusRunning, "container started")
108109

109110
// Verify the status was updated
110111
status, statusContext, err := manager.GetWorkloadStatus(ctx, "test-workload")
111112
require.NoError(t, err)
112-
assert.Equal(t, WorkloadStatusRunning, *status)
113+
assert.Equal(t, runtime.WorkloadStatusRunning, status)
113114
assert.Equal(t, "container started", statusContext)
114115

115116
// Verify the file on disk
@@ -121,7 +122,7 @@ func TestFileStatusManager_SetWorkloadStatus(t *testing.T) {
121122
err = json.Unmarshal(data, &statusFileData)
122123
require.NoError(t, err)
123124

124-
assert.Equal(t, WorkloadStatusRunning, statusFileData.Status)
125+
assert.Equal(t, runtime.WorkloadStatusRunning, statusFileData.Status)
125126
assert.Equal(t, "container started", statusFileData.StatusContext)
126127
// CreatedAt should be preserved, UpdatedAt should be newer
127128
assert.False(t, statusFileData.CreatedAt.IsZero())
@@ -137,7 +138,7 @@ func TestFileStatusManager_SetWorkloadStatus_NotFound(t *testing.T) {
137138
ctx := context.Background()
138139

139140
// Try to set status for non-existent workload - should not error but do nothing
140-
manager.SetWorkloadStatus(ctx, "non-existent", WorkloadStatusRunning, "test")
141+
manager.SetWorkloadStatus(ctx, "non-existent", runtime.WorkloadStatusRunning, "test")
141142

142143
// Verify no file was created
143144
statusFile := filepath.Join(tempDir, "non-existent.json")
@@ -193,7 +194,7 @@ func TestFileStatusManager_ConcurrentAccess(t *testing.T) {
193194
defer func() { done <- true }()
194195
status, statusContext, err := manager.GetWorkloadStatus(ctx, "test-workload")
195196
assert.NoError(t, err)
196-
assert.Equal(t, WorkloadStatusStarting, *status)
197+
assert.Equal(t, runtime.WorkloadStatusStarting, status)
197198
assert.Empty(t, statusContext)
198199
}()
199200
}
@@ -223,31 +224,31 @@ func TestFileStatusManager_FullLifecycle(t *testing.T) {
223224
// 2. Verify initial status
224225
status, statusContext, err := manager.GetWorkloadStatus(ctx, workloadName)
225226
require.NoError(t, err)
226-
assert.Equal(t, WorkloadStatusStarting, *status)
227+
assert.Equal(t, runtime.WorkloadStatusStarting, status)
227228
assert.Empty(t, statusContext)
228229

229230
// 3. Update to running
230-
manager.SetWorkloadStatus(ctx, workloadName, WorkloadStatusRunning, "started successfully")
231+
manager.SetWorkloadStatus(ctx, workloadName, runtime.WorkloadStatusRunning, "started successfully")
231232

232233
status, statusContext, err = manager.GetWorkloadStatus(ctx, workloadName)
233234
require.NoError(t, err)
234-
assert.Equal(t, WorkloadStatusRunning, *status)
235+
assert.Equal(t, runtime.WorkloadStatusRunning, status)
235236
assert.Equal(t, "started successfully", statusContext)
236237

237238
// 4. Update to stopping
238-
manager.SetWorkloadStatus(ctx, workloadName, WorkloadStatusStopping, "shutdown initiated")
239+
manager.SetWorkloadStatus(ctx, workloadName, runtime.WorkloadStatusStopping, "shutdown initiated")
239240

240241
status, statusContext, err = manager.GetWorkloadStatus(ctx, workloadName)
241242
require.NoError(t, err)
242-
assert.Equal(t, WorkloadStatusStopping, *status)
243+
assert.Equal(t, runtime.WorkloadStatusStopping, status)
243244
assert.Equal(t, "shutdown initiated", statusContext)
244245

245246
// 5. Update to stopped
246-
manager.SetWorkloadStatus(ctx, workloadName, WorkloadStatusStopped, "shutdown complete")
247+
manager.SetWorkloadStatus(ctx, workloadName, runtime.WorkloadStatusStopped, "shutdown complete")
247248

248249
status, statusContext, err = manager.GetWorkloadStatus(ctx, workloadName)
249250
require.NoError(t, err)
250-
assert.Equal(t, WorkloadStatusStopped, *status)
251+
assert.Equal(t, runtime.WorkloadStatusStopped, status)
251252
assert.Equal(t, "shutdown complete", statusContext)
252253

253254
// 6. Delete workload

0 commit comments

Comments
 (0)