client: refactor task responses

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Austin Vazquez
2025-10-20 14:30:20 -05:00
committed by Sebastiaan van Stijn
parent 7066eb3736
commit 38ef4fd576
17 changed files with 100 additions and 98 deletions

View File

@@ -168,8 +168,8 @@ type ServiceAPIClient interface {
ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error)
ServiceLogs(ctx context.Context, serviceID string, options ContainerLogsOptions) (io.ReadCloser, error)
TaskLogs(ctx context.Context, taskID string, options ContainerLogsOptions) (io.ReadCloser, error)
TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error)
TaskList(ctx context.Context, options TaskListOptions) ([]swarm.Task, error)
TaskInspect(ctx context.Context, taskID string) (TaskInspectResult, error)
TaskList(ctx context.Context, options TaskListOptions) (TaskListResult, error)
}
// SwarmAPIClient defines API client methods for the swarm

View File

@@ -1,6 +0,0 @@
package client
// TaskListOptions holds parameters to list tasks with.
type TaskListOptions struct {
Filters Filters
}

View File

@@ -1,34 +1,31 @@
package client
import (
"bytes"
"context"
"encoding/json"
"io"
"github.com/moby/moby/api/types/swarm"
)
// TaskInspectWithRaw returns the task information and its raw representation.
func (cli *Client) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) {
// TaskInspectResult contains the result of a task inspection.
type TaskInspectResult struct {
Task swarm.Task
Raw []byte
}
// TaskInspect returns the task information and its raw representation.
func (cli *Client) TaskInspect(ctx context.Context, taskID string) (TaskInspectResult, error) {
taskID, err := trimID("task", taskID)
if err != nil {
return swarm.Task{}, nil, err
return TaskInspectResult{}, err
}
resp, err := cli.get(ctx, "/tasks/"+taskID, nil, nil)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.Task{}, nil, err
return TaskInspectResult{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return swarm.Task{}, nil, err
}
var response swarm.Task
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
var out TaskInspectResult
out.Raw, err = decodeWithRaw(resp, &out.Task)
return out, err
}

View File

@@ -19,7 +19,7 @@ func TestTaskInspectError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
_, _, err = client.TaskInspectWithRaw(context.Background(), "nothing")
_, err = client.TaskInspect(context.Background(), "nothing")
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
@@ -28,11 +28,11 @@ func TestTaskInspectWithEmptyID(t *testing.T) {
return nil, errors.New("should not make request")
}))
assert.NilError(t, err)
_, _, err = client.TaskInspectWithRaw(context.Background(), "")
_, err = client.TaskInspect(context.Background(), "")
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, _, err = client.TaskInspectWithRaw(context.Background(), " ")
_, err = client.TaskInspect(context.Background(), " ")
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -56,7 +56,7 @@ func TestTaskInspect(t *testing.T) {
}))
assert.NilError(t, err)
taskInspect, _, err := client.TaskInspectWithRaw(context.Background(), "task_id")
result, err := client.TaskInspect(context.Background(), "task_id")
assert.NilError(t, err)
assert.Check(t, is.Equal(taskInspect.ID, "task_id"))
assert.Check(t, is.Equal(result.Task.ID, "task_id"))
}

View File

@@ -8,8 +8,18 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// TaskListOptions holds parameters to list tasks with.
type TaskListOptions struct {
Filters Filters
}
// TaskListResult contains the result of a task list operation.
type TaskListResult struct {
Tasks []swarm.Task
}
// TaskList returns the list of tasks.
func (cli *Client) TaskList(ctx context.Context, options TaskListOptions) ([]swarm.Task, error) {
func (cli *Client) TaskList(ctx context.Context, options TaskListOptions) (TaskListResult, error) {
query := url.Values{}
options.Filters.updateURLValues(query)
@@ -17,10 +27,10 @@ func (cli *Client) TaskList(ctx context.Context, options TaskListOptions) ([]swa
resp, err := cli.get(ctx, "/tasks", query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
return TaskListResult{}, err
}
var tasks []swarm.Task
err = json.NewDecoder(resp.Body).Decode(&tasks)
return tasks, err
return TaskListResult{Tasks: tasks}, err
}

View File

@@ -75,8 +75,8 @@ func TestTaskList(t *testing.T) {
}))
assert.NilError(t, err)
tasks, err := client.TaskList(context.Background(), listCase.options)
result, err := client.TaskList(context.Background(), listCase.options)
assert.NilError(t, err)
assert.Check(t, is.Len(tasks, 2))
assert.Check(t, is.Len(result.Tasks, 2))
}
}

View File

@@ -103,13 +103,13 @@ func (d *Daemon) CheckRunningTaskNetworks(ctx context.Context) func(t *testing.T
cli := d.NewClientT(t)
defer cli.Close()
tasks, err := cli.TaskList(ctx, client.TaskListOptions{
taskResult, err := cli.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("desired-state", "running"),
})
assert.NilError(t, err)
result := make(map[string]int)
for _, task := range tasks {
for _, task := range taskResult.Tasks {
for _, network := range task.Spec.Networks {
result[network.Target]++
}
@@ -124,13 +124,13 @@ func (d *Daemon) CheckRunningTaskImages(ctx context.Context) func(t *testing.T)
cli := d.NewClientT(t)
defer cli.Close()
tasks, err := cli.TaskList(ctx, client.TaskListOptions{
taskResult, err := cli.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("desired-state", "running"),
})
assert.NilError(t, err)
result := make(map[string]int)
for _, task := range tasks {
for _, task := range taskResult.Tasks {
if task.Status.State == swarm.TaskStateRunning && task.Spec.ContainerSpec != nil {
result[task.Spec.ContainerSpec.Image]++
}

View File

@@ -204,14 +204,14 @@ func ServiceWithPidsLimit(limit int64) ServiceSpecOpt {
func GetRunningTasks(ctx context.Context, t *testing.T, c client.ServiceAPIClient, serviceID string) []swarmtypes.Task {
t.Helper()
tasks, err := c.TaskList(ctx, client.TaskListOptions{
result, err := c.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).
Add("service", serviceID).
Add("desired-state", "running"),
})
assert.NilError(t, err)
return tasks
return result.Tasks
}
// ExecTask runs the passed in exec config on the given task

View File

@@ -12,15 +12,15 @@ import (
// NoTasksForService verifies that there are no more tasks for the given service
func NoTasksForService(ctx context.Context, apiClient client.ServiceAPIClient, serviceID string) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result {
tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{
result, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID),
})
if err == nil {
if len(tasks) == 0 {
if len(result.Tasks) == 0 {
return poll.Success()
}
if len(tasks) > 0 {
return poll.Continue("task count for service %s at %d waiting for 0", serviceID, len(tasks))
if len(result.Tasks) > 0 {
return poll.Continue("task count for service %s at %d waiting for 0", serviceID, len(result.Tasks))
}
return poll.Continue("waiting for tasks for service %s to be deleted", serviceID)
}
@@ -32,14 +32,14 @@ func NoTasksForService(ctx context.Context, apiClient client.ServiceAPIClient, s
// NoTasks verifies that all tasks are gone
func NoTasks(ctx context.Context, apiClient client.ServiceAPIClient) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result {
tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{})
result, err := apiClient.TaskList(ctx, client.TaskListOptions{})
switch {
case err != nil:
return poll.Error(err)
case len(tasks) == 0:
case len(result.Tasks) == 0:
return poll.Success()
default:
return poll.Continue("waiting for all tasks to be removed: task count at %d", len(tasks))
return poll.Continue("waiting for all tasks to be removed: task count at %d", len(result.Tasks))
}
}
}
@@ -47,12 +47,12 @@ func NoTasks(ctx context.Context, apiClient client.ServiceAPIClient) func(log po
// RunningTasksCount verifies there are `instances` tasks running for `serviceID`
func RunningTasksCount(ctx context.Context, apiClient client.ServiceAPIClient, serviceID string, instances uint64) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result {
tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{
result, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID),
})
var running int
var taskError string
for _, task := range tasks {
for _, task := range result.Tasks {
switch task.Status.State {
case swarmtypes.TaskStateRunning:
running++
@@ -76,7 +76,7 @@ func RunningTasksCount(ctx context.Context, apiClient client.ServiceAPIClient, s
case running == int(instances):
return poll.Success()
default:
return poll.Continue("running task count at %d waiting for %d (total tasks: %d)", running, instances, len(tasks))
return poll.Continue("running task count at %d waiting for %d (total tasks: %d)", running, instances, len(result.Tasks))
}
}
}
@@ -97,7 +97,7 @@ func JobComplete(ctx context.Context, apiClient client.ServiceAPIClient, service
previousResult := ""
return func(log poll.LogT) poll.Result {
tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{
result, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: filter,
})
if err != nil {
@@ -110,7 +110,7 @@ func JobComplete(ctx context.Context, apiClient client.ServiceAPIClient, service
var runningSlot []int
var runningID []string
for _, task := range tasks {
for _, task := range result.Tasks {
// make sure the task has the same job iteration
if task.JobIteration == nil || task.JobIteration.Index != jobIteration.Index {
continue

View File

@@ -368,19 +368,19 @@ func TestCreateServiceSysctls(t *testing.T) {
// more complex)
// get all tasks of the service, so we can get the container
tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{
taskResult, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID),
})
assert.NilError(t, err)
assert.Check(t, is.Equal(len(tasks), 1))
assert.Check(t, is.Equal(len(taskResult.Tasks), 1))
// verify that the container has the sysctl option set
ctnr, err := apiClient.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID)
ctnr, err := apiClient.ContainerInspect(ctx, taskResult.Tasks[0].Status.ContainerStatus.ContainerID)
assert.NilError(t, err)
assert.DeepEqual(t, ctnr.HostConfig.Sysctls, expectedSysctls)
// verify that the task has the sysctl option set in the task object
assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.Sysctls, expectedSysctls)
assert.DeepEqual(t, taskResult.Tasks[0].Spec.ContainerSpec.Sysctls, expectedSysctls)
// verify that the service also has the sysctl set in the spec.
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{})
@@ -438,21 +438,21 @@ func TestCreateServiceCapabilities(t *testing.T) {
// level has been tested elsewhere.
// get all tasks of the service, so we can get the container
tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{
taskResult, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID),
})
assert.NilError(t, err)
assert.Check(t, is.Equal(len(tasks), 1))
assert.Check(t, is.Equal(len(taskResult.Tasks), 1))
// verify that the container has the capabilities option set
ctnr, err := apiClient.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID)
ctnr, err := apiClient.ContainerInspect(ctx, taskResult.Tasks[0].Status.ContainerStatus.ContainerID)
assert.NilError(t, err)
assert.DeepEqual(t, ctnr.HostConfig.CapAdd, capAdd)
assert.DeepEqual(t, ctnr.HostConfig.CapDrop, capDrop)
// verify that the task has the capabilities option set in the task object
assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.CapabilityAdd, capAdd)
assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.CapabilityDrop, capDrop)
assert.DeepEqual(t, taskResult.Tasks[0].Spec.ContainerSpec.CapabilityAdd, capAdd)
assert.DeepEqual(t, taskResult.Tasks[0].Spec.ContainerSpec.CapabilityDrop, capDrop)
// verify that the service also has the capabilities set in the spec.
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{})

View File

@@ -52,12 +52,12 @@ func TestServiceListWithStatuses(t *testing.T) {
// serviceContainerCount function does not do. instead, we'll use a
// bespoke closure right here.
poll.WaitOn(t, func(log poll.LogT) poll.Result {
tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{
taskResult, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", id),
})
running := 0
for _, task := range tasks {
for _, task := range taskResult.Tasks {
if task.Status.State == swarmtypes.TaskStateRunning {
running++
}
@@ -71,7 +71,7 @@ func TestServiceListWithStatuses(t *testing.T) {
default:
return poll.Continue(
"running task count %d (%d total), waiting for %d",
running, len(tasks), i+1,
running, len(taskResult.Tasks), i+1,
)
}
})

View File

@@ -325,13 +325,13 @@ func TestServiceUpdatePidsLimit(t *testing.T) {
func getServiceTaskContainer(ctx context.Context, t *testing.T, cli client.APIClient, serviceID string) container.InspectResponse {
t.Helper()
tasks, err := cli.TaskList(ctx, client.TaskListOptions{
taskResult, err := cli.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID).Add("desired-state", "running"),
})
assert.NilError(t, err)
assert.Assert(t, len(tasks) > 0)
assert.Assert(t, len(taskResult.Tasks) > 0)
ctr, err := cli.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID)
ctr, err := cli.ContainerInspect(ctx, taskResult.Tasks[0].Status.ContainerStatus.ContainerID)
assert.NilError(t, err)
assert.Equal(t, ctr.State.Running, true)
return ctr

View File

@@ -70,9 +70,9 @@ func (d *Daemon) GetServiceTasksWithFilters(ctx context.Context, t testing.TB, s
Filters: filterArgs,
}
tasks, err := cli.TaskList(ctx, options)
result, err := cli.TaskList(ctx, options)
assert.NilError(t, err)
return tasks
return result.Tasks
}
// UpdateService updates a swarm service with the specified service constructor
@@ -116,7 +116,7 @@ func (d *Daemon) GetTask(ctx context.Context, t testing.TB, id string) swarm.Tas
cli := d.NewClientT(t)
defer cli.Close()
task, _, err := cli.TaskInspectWithRaw(ctx, id)
result, err := cli.TaskInspect(ctx, id)
assert.NilError(t, err)
return task
return result.Task
}

View File

@@ -168,8 +168,8 @@ type ServiceAPIClient interface {
ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error)
ServiceLogs(ctx context.Context, serviceID string, options ContainerLogsOptions) (io.ReadCloser, error)
TaskLogs(ctx context.Context, taskID string, options ContainerLogsOptions) (io.ReadCloser, error)
TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error)
TaskList(ctx context.Context, options TaskListOptions) ([]swarm.Task, error)
TaskInspect(ctx context.Context, taskID string) (TaskInspectResult, error)
TaskList(ctx context.Context, options TaskListOptions) (TaskListResult, error)
}
// SwarmAPIClient defines API client methods for the swarm

View File

@@ -1,6 +0,0 @@
package client
// TaskListOptions holds parameters to list tasks with.
type TaskListOptions struct {
Filters Filters
}

View File

@@ -1,34 +1,31 @@
package client
import (
"bytes"
"context"
"encoding/json"
"io"
"github.com/moby/moby/api/types/swarm"
)
// TaskInspectWithRaw returns the task information and its raw representation.
func (cli *Client) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) {
// TaskInspectResult contains the result of a task inspection.
type TaskInspectResult struct {
Task swarm.Task
Raw []byte
}
// TaskInspect returns the task information and its raw representation.
func (cli *Client) TaskInspect(ctx context.Context, taskID string) (TaskInspectResult, error) {
taskID, err := trimID("task", taskID)
if err != nil {
return swarm.Task{}, nil, err
return TaskInspectResult{}, err
}
resp, err := cli.get(ctx, "/tasks/"+taskID, nil, nil)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.Task{}, nil, err
return TaskInspectResult{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return swarm.Task{}, nil, err
}
var response swarm.Task
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
var out TaskInspectResult
out.Raw, err = decodeWithRaw(resp, &out.Task)
return out, err
}

View File

@@ -8,8 +8,18 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// TaskListOptions holds parameters to list tasks with.
type TaskListOptions struct {
Filters Filters
}
// TaskListResult contains the result of a task list operation.
type TaskListResult struct {
Tasks []swarm.Task
}
// TaskList returns the list of tasks.
func (cli *Client) TaskList(ctx context.Context, options TaskListOptions) ([]swarm.Task, error) {
func (cli *Client) TaskList(ctx context.Context, options TaskListOptions) (TaskListResult, error) {
query := url.Values{}
options.Filters.updateURLValues(query)
@@ -17,10 +27,10 @@ func (cli *Client) TaskList(ctx context.Context, options TaskListOptions) ([]swa
resp, err := cli.get(ctx, "/tasks", query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
return TaskListResult{}, err
}
var tasks []swarm.Task
err = json.NewDecoder(resp.Body).Decode(&tasks)
return tasks, err
return TaskListResult{Tasks: tasks}, err
}