client: refactor service api client functions for defined options/result structs

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
This commit is contained in:
Austin Vazquez
2025-10-21 14:21:54 -05:00
parent 13374b5a72
commit cd08b79c02
50 changed files with 646 additions and 399 deletions

View File

@@ -160,14 +160,14 @@ type PluginAPIClient interface {
// ServiceAPIClient defines API client methods for the services
type ServiceAPIClient interface {
ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options ServiceCreateOptions) (swarm.ServiceCreateResponse, error)
ServiceInspectWithRaw(ctx context.Context, serviceID string, options ServiceInspectOptions) (swarm.Service, []byte, error)
ServiceList(ctx context.Context, options ServiceListOptions) ([]swarm.Service, error)
ServiceRemove(ctx context.Context, serviceID string) error
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)
TaskInspect(ctx context.Context, taskID string) (TaskInspectResult, error)
ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options ServiceCreateOptions) (ServiceCreateResult, error)
ServiceInspect(ctx context.Context, serviceID string, options ServiceInspectOptions) (ServiceInspectResult, error)
ServiceList(ctx context.Context, options ServiceListOptions) (ServiceListResult, error)
ServiceRemove(ctx context.Context, serviceID string, options ServiceRemoveOptions) (ServiceRemoveResult, error)
ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options ServiceUpdateOptions) (ServiceUpdateResult, error)
ServiceLogs(ctx context.Context, serviceID string, options ServiceLogsOptions) (ServiceLogsResult, error)
TaskLogs(ctx context.Context, taskID string, options TaskLogsOptions) (TaskLogsResult, error)
TaskInspect(ctx context.Context, taskID string, options TaskInspectOptions) (TaskInspectResult, error)
TaskList(ctx context.Context, options TaskListOptions) (TaskListResult, error)
}

View File

@@ -14,35 +14,59 @@ import (
"github.com/opencontainers/go-digest"
)
// ServiceCreate creates a new service.
func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options ServiceCreateOptions) (swarm.ServiceCreateResponse, error) {
var response swarm.ServiceCreateResponse
// ServiceCreateOptions contains the options to use when creating a service.
type ServiceCreateOptions struct {
// EncodedRegistryAuth is the encoded registry authorization credentials to
// use when updating the service.
//
// This field follows the format of the X-Registry-Auth header.
EncodedRegistryAuth string
// QueryRegistry indicates whether the service update requires
// contacting a registry. A registry may be contacted to retrieve
// the image digest and manifest, which in turn can be used to update
// platform or other information about the service.
QueryRegistry bool
}
// ServiceCreateResult represents the result of creating a service.
type ServiceCreateResult struct {
// ID is the ID of the created service.
ID string
// Warnings is a list of warnings that occurred during service creation.
Warnings []string
}
// ServiceCreate creates a new service.
func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options ServiceCreateOptions) (ServiceCreateResult, error) {
// Make sure containerSpec is not nil when no runtime is set or the runtime is set to container
if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) {
service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
}
if err := validateServiceSpec(service); err != nil {
return response, err
return ServiceCreateResult{}, err
}
// ensure that the image is tagged
var resolveWarning string
var warnings []string
switch {
case service.TaskTemplate.ContainerSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
service.TaskTemplate.ContainerSpec.Image = taggedImg
}
if options.QueryRegistry {
resolveWarning = resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
resolveWarning := resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
warnings = append(warnings, resolveWarning)
}
case service.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
service.TaskTemplate.PluginSpec.Remote = taggedImg
}
if options.QueryRegistry {
resolveWarning = resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
resolveWarning := resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
warnings = append(warnings, resolveWarning)
}
}
@@ -53,15 +77,17 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec,
resp, err := cli.post(ctx, "/services/create", nil, service, headers)
defer ensureReaderClosed(resp)
if err != nil {
return response, err
return ServiceCreateResult{}, err
}
var response swarm.ServiceCreateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if resolveWarning != "" {
response.Warnings = append(response.Warnings, resolveWarning)
}
warnings = append(warnings, response.Warnings...)
return response, err
return ServiceCreateResult{
ID: response.ID,
Warnings: warnings,
}, err
}
func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {

View File

@@ -1,38 +1,40 @@
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"github.com/moby/moby/api/types/swarm"
)
// ServiceInspectWithRaw returns the service information and the raw data.
func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string, opts ServiceInspectOptions) (swarm.Service, []byte, error) {
// ServiceInspectOptions holds parameters related to the service inspect operation.
type ServiceInspectOptions struct {
InsertDefaults bool
}
// ServiceInspectResult represents the result of a service inspect operation.
type ServiceInspectResult struct {
Service swarm.Service
Raw []byte
}
// ServiceInspect retrieves detailed information about a specific service by its ID.
func (cli *Client) ServiceInspect(ctx context.Context, serviceID string, options ServiceInspectOptions) (ServiceInspectResult, error) {
serviceID, err := trimID("service", serviceID)
if err != nil {
return swarm.Service{}, nil, err
return ServiceInspectResult{}, err
}
query := url.Values{}
query.Set("insertDefaults", fmt.Sprintf("%v", opts.InsertDefaults))
query.Set("insertDefaults", fmt.Sprintf("%v", options.InsertDefaults))
resp, err := cli.get(ctx, "/services/"+serviceID, query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.Service{}, nil, err
return ServiceInspectResult{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return swarm.Service{}, nil, err
}
var response swarm.Service
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
var out ServiceInspectResult
out.Raw, err = decodeWithRaw(resp, &out.Service)
return out, err
}

View File

@@ -19,7 +19,7 @@ func TestServiceInspectError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
_, _, err = client.ServiceInspectWithRaw(context.Background(), "nothing", ServiceInspectOptions{})
_, err = client.ServiceInspect(context.Background(), "nothing", ServiceInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
@@ -27,7 +27,7 @@ func TestServiceInspectServiceNotFound(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusNotFound, "Server error")))
assert.NilError(t, err)
_, _, err = client.ServiceInspectWithRaw(context.Background(), "unknown", ServiceInspectOptions{})
_, err = client.ServiceInspect(context.Background(), "unknown", ServiceInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
}
@@ -36,11 +36,11 @@ func TestServiceInspectWithEmptyID(t *testing.T) {
return nil, errors.New("should not make request")
}))
assert.NilError(t, err)
_, _, err = client.ServiceInspectWithRaw(context.Background(), "", ServiceInspectOptions{})
_, err = client.ServiceInspect(context.Background(), "", ServiceInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, _, err = client.ServiceInspectWithRaw(context.Background(), " ", ServiceInspectOptions{})
_, err = client.ServiceInspect(context.Background(), " ", ServiceInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -64,7 +64,7 @@ func TestServiceInspect(t *testing.T) {
}))
assert.NilError(t, err)
serviceInspect, _, err := client.ServiceInspectWithRaw(context.Background(), "service_id", ServiceInspectOptions{})
inspect, err := client.ServiceInspect(context.Background(), "service_id", ServiceInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(serviceInspect.ID, "service_id"))
assert.Check(t, is.Equal(inspect.Service.ID, "service_id"))
}

View File

@@ -8,8 +8,22 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// ServiceListOptions holds parameters to list services with.
type ServiceListOptions struct {
Filters Filters
// Status indicates whether the server should include the service task
// count of running and desired tasks.
Status bool
}
// ServiceListResult represents the result of a service list operation.
type ServiceListResult struct {
Services []swarm.Service
}
// ServiceList returns the list of services.
func (cli *Client) ServiceList(ctx context.Context, options ServiceListOptions) ([]swarm.Service, error) {
func (cli *Client) ServiceList(ctx context.Context, options ServiceListOptions) (ServiceListResult, error) {
query := url.Values{}
options.Filters.updateURLValues(query)
@@ -21,10 +35,10 @@ func (cli *Client) ServiceList(ctx context.Context, options ServiceListOptions)
resp, err := cli.get(ctx, "/services", query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
return ServiceListResult{}, err
}
var services []swarm.Service
err = json.NewDecoder(resp.Body).Decode(&services)
return services, err
return ServiceListResult{Services: services}, err
}

View File

@@ -75,8 +75,8 @@ func TestServiceList(t *testing.T) {
}))
assert.NilError(t, err)
services, err := client.ServiceList(context.Background(), listCase.options)
list, err := client.ServiceList(context.Background(), listCase.options)
assert.NilError(t, err)
assert.Check(t, is.Len(services, 2))
assert.Check(t, is.Len(list.Services, 2))
}
}

View File

@@ -5,17 +5,37 @@ import (
"fmt"
"io"
"net/url"
"sync"
"time"
"github.com/moby/moby/client/internal/timestamp"
)
// ServiceLogs returns the logs generated by a service in an [io.ReadCloser].
// ServiceLogsOptions holds parameters to filter logs with.
type ServiceLogsOptions struct {
ShowStdout bool
ShowStderr bool
Since string
Until string
Timestamps bool
Follow bool
Tail string
Details bool
}
// ServiceLogsResult holds the result of a service logs operation.
// It implements [io.ReadCloser].
// It's up to the caller to close the stream.
func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options ContainerLogsOptions) (io.ReadCloser, error) {
type ServiceLogsResult struct {
rc io.ReadCloser
close func() error
}
// ServiceLogs returns the logs generated by a service in an [ServiceLogsResult].
func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options ServiceLogsOptions) (ServiceLogsResult, error) {
serviceID, err := trimID("service", serviceID)
if err != nil {
return nil, err
return ServiceLogsResult{}, err
}
query := url.Values{}
@@ -30,7 +50,7 @@ func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options Co
if options.Since != "" {
ts, err := timestamp.GetTimestamp(options.Since, time.Now())
if err != nil {
return nil, fmt.Errorf(`invalid value for "since": %w`, err)
return ServiceLogsResult{}, fmt.Errorf(`invalid value for "since": %w`, err)
}
query.Set("since", ts)
}
@@ -50,7 +70,33 @@ func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options Co
resp, err := cli.get(ctx, "/services/"+serviceID+"/logs", query, nil)
if err != nil {
return nil, err
return ServiceLogsResult{}, err
}
return resp.Body, nil
return newServiceLogsResult(resp.Body), nil
}
func newServiceLogsResult(rc io.ReadCloser) ServiceLogsResult {
if rc == nil {
panic("nil io.ReadCloser")
}
return ServiceLogsResult{
rc: rc,
close: sync.OnceValue(rc.Close),
}
}
// Read implements [io.ReadCloser] for LogsResult.
func (r ServiceLogsResult) Read(p []byte) (n int, err error) {
if r.rc == nil {
return 0, io.EOF
}
return r.rc.Read(p)
}
// Close implements [io.ReadCloser] for LogsResult.
func (r ServiceLogsResult) Close() error {
if r.close == nil {
return nil
}
return r.close()
}

View File

@@ -20,19 +20,19 @@ import (
func TestServiceLogsError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
_, err = client.ServiceLogs(context.Background(), "service_id", ContainerLogsOptions{})
_, err = client.ServiceLogs(context.Background(), "service_id", ServiceLogsOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
_, err = client.ServiceLogs(context.Background(), "service_id", ContainerLogsOptions{
_, err = client.ServiceLogs(context.Background(), "service_id", ServiceLogsOptions{
Since: "2006-01-02TZ",
})
assert.Check(t, is.ErrorContains(err, `parsing time "2006-01-02TZ"`))
_, err = client.ServiceLogs(context.Background(), "", ContainerLogsOptions{})
_, err = client.ServiceLogs(context.Background(), "", ServiceLogsOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, err = client.ServiceLogs(context.Background(), " ", ContainerLogsOptions{})
_, err = client.ServiceLogs(context.Background(), " ", ServiceLogsOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -40,7 +40,7 @@ func TestServiceLogsError(t *testing.T) {
func TestServiceLogs(t *testing.T) {
const expectedURL = "/services/service_id/logs"
cases := []struct {
options ContainerLogsOptions
options ServiceLogsOptions
expectedQueryParams map[string]string
expectedError string
}{
@@ -50,7 +50,7 @@ func TestServiceLogs(t *testing.T) {
},
},
{
options: ContainerLogsOptions{
options: ServiceLogsOptions{
Tail: "any",
},
expectedQueryParams: map[string]string{
@@ -58,7 +58,7 @@ func TestServiceLogs(t *testing.T) {
},
},
{
options: ContainerLogsOptions{
options: ServiceLogsOptions{
ShowStdout: true,
ShowStderr: true,
Timestamps: true,
@@ -75,7 +75,7 @@ func TestServiceLogs(t *testing.T) {
},
},
{
options: ContainerLogsOptions{
options: ServiceLogsOptions{
// timestamp is passed as-is
Since: "1136073600.000000001",
},
@@ -85,7 +85,7 @@ func TestServiceLogs(t *testing.T) {
},
},
{
options: ContainerLogsOptions{
options: ServiceLogsOptions{
// invalid dates are not passed.
Since: "invalid value",
},
@@ -129,7 +129,7 @@ func ExampleClient_ServiceLogs_withTimeout() {
defer cancel()
client, _ := NewClientWithOpts(FromEnv)
reader, err := client.ServiceLogs(ctx, "service_id", ContainerLogsOptions{})
reader, err := client.ServiceLogs(ctx, "service_id", ServiceLogsOptions{})
if err != nil {
log.Fatal(err)
}

View File

@@ -2,14 +2,24 @@ package client
import "context"
// ServiceRemoveOptions contains options for removing a service.
type ServiceRemoveOptions struct {
// No options currently; placeholder for future use
}
// ServiceRemoveResult contains the result of removing a service.
type ServiceRemoveResult struct {
// No fields currently; placeholder for future use
}
// ServiceRemove kills and removes a service.
func (cli *Client) ServiceRemove(ctx context.Context, serviceID string) error {
func (cli *Client) ServiceRemove(ctx context.Context, serviceID string, options ServiceRemoveOptions) (ServiceRemoveResult, error) {
serviceID, err := trimID("service", serviceID)
if err != nil {
return err
return ServiceRemoveResult{}, err
}
resp, err := cli.delete(ctx, "/services/"+serviceID, nil, nil)
defer ensureReaderClosed(resp)
return err
return ServiceRemoveResult{}, err
}

View File

@@ -16,14 +16,14 @@ func TestServiceRemoveError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.ServiceRemove(context.Background(), "service_id")
_, err = client.ServiceRemove(context.Background(), "service_id", ServiceRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.ServiceRemove(context.Background(), "")
_, err = client.ServiceRemove(context.Background(), "", ServiceRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.ServiceRemove(context.Background(), " ")
_, err = client.ServiceRemove(context.Background(), " ", ServiceRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -32,7 +32,7 @@ func TestServiceRemoveNotFoundError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusNotFound, "no such service: service_id")))
assert.NilError(t, err)
err = client.ServiceRemove(context.Background(), "service_id")
_, err = client.ServiceRemove(context.Background(), "service_id", ServiceRemoveOptions{})
assert.Check(t, is.ErrorContains(err, "no such service: service_id"))
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
}
@@ -51,6 +51,6 @@ func TestServiceRemove(t *testing.T) {
}))
assert.NilError(t, err)
err = client.ServiceRemove(context.Background(), "service_id")
_, err = client.ServiceRemove(context.Background(), "service_id", ServiceRemoveOptions{})
assert.NilError(t, err)
}

View File

@@ -10,18 +10,54 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// ServiceUpdateOptions contains the options to be used for updating services.
type ServiceUpdateOptions struct {
// EncodedRegistryAuth is the encoded registry authorization credentials to
// use when updating the service.
//
// This field follows the format of the X-Registry-Auth header.
EncodedRegistryAuth string
// TODO(stevvooe): Consider moving the version parameter of ServiceUpdate
// into this field. While it does open API users up to racy writes, most
// users may not need that level of consistency in practice.
// RegistryAuthFrom specifies where to find the registry authorization
// credentials if they are not given in EncodedRegistryAuth. Valid
// values are "spec" and "previous-spec".
RegistryAuthFrom string
// Rollback indicates whether a server-side rollback should be
// performed. When this is set, the provided spec will be ignored.
// The valid values are "previous" and "none". An empty value is the
// same as "none".
Rollback string
// QueryRegistry indicates whether the service update requires
// contacting a registry. A registry may be contacted to retrieve
// the image digest and manifest, which in turn can be used to update
// platform or other information about the service.
QueryRegistry bool
}
// ServiceUpdateResult represents the result of a service update.
type ServiceUpdateResult struct {
// Warnings contains any warnings that occurred during the update.
Warnings []string
}
// ServiceUpdate updates a Service. The version number is required to avoid
// conflicting writes. It must be the value as set *before* the update.
// You can find this value in the [swarm.Service.Meta] field, which can
// be found using [Client.ServiceInspectWithRaw].
func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options ServiceUpdateOptions) (ServiceUpdateResult, error) {
serviceID, err := trimID("service", serviceID)
if err != nil {
return swarm.ServiceUpdateResponse{}, err
return ServiceUpdateResult{}, err
}
if err := validateServiceSpec(service); err != nil {
return swarm.ServiceUpdateResponse{}, err
return ServiceUpdateResult{}, err
}
query := url.Values{}
@@ -36,21 +72,23 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
query.Set("version", version.String())
// ensure that the image is tagged
var resolveWarning string
var warnings []string
switch {
case service.TaskTemplate.ContainerSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
service.TaskTemplate.ContainerSpec.Image = taggedImg
}
if options.QueryRegistry {
resolveWarning = resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
resolveWarning := resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
warnings = append(warnings, resolveWarning)
}
case service.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
service.TaskTemplate.PluginSpec.Remote = taggedImg
}
if options.QueryRegistry {
resolveWarning = resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
resolveWarning := resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
warnings = append(warnings, resolveWarning)
}
}
@@ -61,14 +99,11 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.ServiceUpdateResponse{}, err
return ServiceUpdateResult{}, err
}
var response swarm.ServiceUpdateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
if resolveWarning != "" {
response.Warnings = append(response.Warnings, resolveWarning)
}
return response, err
warnings = append(warnings, response.Warnings...)
return ServiceUpdateResult{Warnings: warnings}, err
}

View File

@@ -17,7 +17,9 @@ type SwarmJoinOptions struct {
}
// SwarmJoinResult contains the result of joining a swarm.
type SwarmJoinResult struct{}
type SwarmJoinResult struct {
// No fields currently; placeholder for future use
}
// SwarmJoin joins the swarm.
func (cli *Client) SwarmJoin(ctx context.Context, options SwarmJoinOptions) (SwarmJoinResult, error) {

View File

@@ -1,16 +0,0 @@
package client
// ServiceCreateOptions contains the options to use when creating a service.
type ServiceCreateOptions struct {
// EncodedRegistryAuth is the encoded registry authorization credentials to
// use when updating the service.
//
// This field follows the format of the X-Registry-Auth header.
EncodedRegistryAuth string
// QueryRegistry indicates whether the service update requires
// contacting a registry. A registry may be contacted to retrieve
// the image digest and manifest, which in turn can be used to update
// platform or other information about the service.
QueryRegistry bool
}

View File

@@ -1,7 +0,0 @@
package client
// ServiceInspectOptions holds parameters related to the "service inspect"
// operation.
type ServiceInspectOptions struct {
InsertDefaults bool
}

View File

@@ -1,10 +0,0 @@
package client
// ServiceListOptions holds parameters to list services with.
type ServiceListOptions struct {
Filters Filters
// Status indicates whether the server should include the service task
// count of running and desired tasks.
Status bool
}

View File

@@ -1,31 +0,0 @@
package client
// ServiceUpdateOptions contains the options to be used for updating services.
type ServiceUpdateOptions struct {
// EncodedRegistryAuth is the encoded registry authorization credentials to
// use when updating the service.
//
// This field follows the format of the X-Registry-Auth header.
EncodedRegistryAuth string
// TODO(stevvooe): Consider moving the version parameter of ServiceUpdate
// into this field. While it does open API users up to racy writes, most
// users may not need that level of consistency in practice.
// RegistryAuthFrom specifies where to find the registry authorization
// credentials if they are not given in EncodedRegistryAuth. Valid
// values are "spec" and "previous-spec".
RegistryAuthFrom string
// Rollback indicates whether a server-side rollback should be
// performed. When this is set, the provided spec will be ignored.
// The valid values are "previous" and "none". An empty value is the
// same as "none".
Rollback string
// QueryRegistry indicates whether the service update requires
// contacting a registry. A registry may be contacted to retrieve
// the image digest and manifest, which in turn can be used to update
// platform or other information about the service.
QueryRegistry bool
}

View File

@@ -6,6 +6,11 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// TaskInspectOptions contains options for inspecting a task.
type TaskInspectOptions struct {
// Currently no options are defined.
}
// TaskInspectResult contains the result of a task inspection.
type TaskInspectResult struct {
Task swarm.Task
@@ -13,7 +18,7 @@ type TaskInspectResult struct {
}
// TaskInspect returns the task information and its raw representation.
func (cli *Client) TaskInspect(ctx context.Context, taskID string) (TaskInspectResult, error) {
func (cli *Client) TaskInspect(ctx context.Context, taskID string, options TaskInspectOptions) (TaskInspectResult, error) {
taskID, err := trimID("task", taskID)
if err != nil {
return TaskInspectResult{}, 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.TaskInspect(context.Background(), "nothing")
_, err = client.TaskInspect(context.Background(), "nothing", TaskInspectOptions{})
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.TaskInspect(context.Background(), "")
_, err = client.TaskInspect(context.Background(), "", TaskInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, err = client.TaskInspect(context.Background(), " ")
_, err = client.TaskInspect(context.Background(), " ", TaskInspectOptions{})
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)
result, err := client.TaskInspect(context.Background(), "task_id")
result, err := client.TaskInspect(context.Background(), "task_id", TaskInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(result.Task.ID, "task_id"))
}

View File

@@ -15,7 +15,7 @@ type TaskListOptions struct {
// TaskListResult contains the result of a task list operation.
type TaskListResult struct {
Tasks []swarm.Task
Items []swarm.Task
}
// TaskList returns the list of tasks.
@@ -32,5 +32,5 @@ func (cli *Client) TaskList(ctx context.Context, options TaskListOptions) (TaskL
var tasks []swarm.Task
err = json.NewDecoder(resp.Body).Decode(&tasks)
return TaskListResult{Tasks: tasks}, err
return TaskListResult{Items: tasks}, err
}

View File

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

View File

@@ -4,14 +4,34 @@ import (
"context"
"io"
"net/url"
"sync"
"time"
"github.com/moby/moby/client/internal/timestamp"
)
// TaskLogs returns the logs generated by a task in an [io.ReadCloser].
// TaskLogsOptions holds parameters to filter logs with.
type TaskLogsOptions struct {
ShowStdout bool
ShowStderr bool
Since string
Until string
Timestamps bool
Follow bool
Tail string
Details bool
}
// TaskLogsResult holds the result of a task logs operation.
// It implements [io.ReadCloser].
type TaskLogsResult struct {
rc io.ReadCloser
close func() error
}
// TaskLogs returns the logs generated by a task.
// It's up to the caller to close the stream.
func (cli *Client) TaskLogs(ctx context.Context, taskID string, options ContainerLogsOptions) (io.ReadCloser, error) {
func (cli *Client) TaskLogs(ctx context.Context, taskID string, options TaskLogsOptions) (TaskLogsResult, error) {
query := url.Values{}
if options.ShowStdout {
query.Set("stdout", "1")
@@ -24,7 +44,7 @@ func (cli *Client) TaskLogs(ctx context.Context, taskID string, options Containe
if options.Since != "" {
ts, err := timestamp.GetTimestamp(options.Since, time.Now())
if err != nil {
return nil, err
return TaskLogsResult{}, err
}
query.Set("since", ts)
}
@@ -44,7 +64,33 @@ func (cli *Client) TaskLogs(ctx context.Context, taskID string, options Containe
resp, err := cli.get(ctx, "/tasks/"+taskID+"/logs", query, nil)
if err != nil {
return nil, err
return TaskLogsResult{}, err
}
return resp.Body, nil
return newTaskLogsResult(resp.Body), nil
}
func newTaskLogsResult(rc io.ReadCloser) TaskLogsResult {
if rc == nil {
panic("nil io.ReadCloser")
}
return TaskLogsResult{
rc: rc,
close: sync.OnceValue(rc.Close),
}
}
// Read implements [io.ReadCloser] for LogsResult.
func (r TaskLogsResult) Read(p []byte) (n int, err error) {
if r.rc == nil {
return 0, io.EOF
}
return r.rc.Read(p)
}
// Close implements [io.ReadCloser] for LogsResult.
func (r TaskLogsResult) Close() error {
if r.close == nil {
return nil
}
return r.close()
}