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

View File

@@ -14,35 +14,59 @@ import (
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
) )
// ServiceCreate creates a new service. // ServiceCreateOptions contains the options to use when creating a service.
func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options ServiceCreateOptions) (swarm.ServiceCreateResponse, error) { type ServiceCreateOptions struct {
var response swarm.ServiceCreateResponse // 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 // 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) { if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) {
service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{} service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
} }
if err := validateServiceSpec(service); err != nil { if err := validateServiceSpec(service); err != nil {
return response, err return ServiceCreateResult{}, err
} }
// ensure that the image is tagged // ensure that the image is tagged
var resolveWarning string var warnings []string
switch { switch {
case service.TaskTemplate.ContainerSpec != nil: case service.TaskTemplate.ContainerSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" { if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
service.TaskTemplate.ContainerSpec.Image = taggedImg service.TaskTemplate.ContainerSpec.Image = taggedImg
} }
if options.QueryRegistry { 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: case service.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" { if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
service.TaskTemplate.PluginSpec.Remote = taggedImg service.TaskTemplate.PluginSpec.Remote = taggedImg
} }
if options.QueryRegistry { 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) resp, err := cli.post(ctx, "/services/create", nil, service, headers)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return response, err return ServiceCreateResult{}, err
} }
var response swarm.ServiceCreateResponse
err = json.NewDecoder(resp.Body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
if resolveWarning != "" { warnings = append(warnings, response.Warnings...)
response.Warnings = append(response.Warnings, resolveWarning)
}
return response, err return ServiceCreateResult{
ID: response.ID,
Warnings: warnings,
}, err
} }
func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {

View File

@@ -1,38 +1,40 @@
package client package client
import ( import (
"bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io"
"net/url" "net/url"
"github.com/moby/moby/api/types/swarm" "github.com/moby/moby/api/types/swarm"
) )
// ServiceInspectWithRaw returns the service information and the raw data. // ServiceInspectOptions holds parameters related to the service inspect operation.
func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string, opts ServiceInspectOptions) (swarm.Service, []byte, error) { 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) serviceID, err := trimID("service", serviceID)
if err != nil { if err != nil {
return swarm.Service{}, nil, err return ServiceInspectResult{}, err
} }
query := url.Values{} 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) resp, err := cli.get(ctx, "/services/"+serviceID, query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return swarm.Service{}, nil, err return ServiceInspectResult{}, err
} }
body, err := io.ReadAll(resp.Body) var out ServiceInspectResult
if err != nil { out.Raw, err = decodeWithRaw(resp, &out.Service)
return swarm.Service{}, nil, err return out, err
}
var response swarm.Service
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
} }

View File

@@ -19,7 +19,7 @@ func TestServiceInspectError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) 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)) 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"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusNotFound, "Server error")))
assert.NilError(t, err) 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)) 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") return nil, errors.New("should not make request")
})) }))
assert.NilError(t, err) 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.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) 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.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -64,7 +64,7 @@ func TestServiceInspect(t *testing.T) {
})) }))
assert.NilError(t, err) 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.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" "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. // 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{} query := url.Values{}
options.Filters.updateURLValues(query) 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) resp, err := cli.get(ctx, "/services", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return nil, err return ServiceListResult{}, err
} }
var services []swarm.Service var services []swarm.Service
err = json.NewDecoder(resp.Body).Decode(&services) 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) 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.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" "fmt"
"io" "io"
"net/url" "net/url"
"sync"
"time" "time"
"github.com/moby/moby/client/internal/timestamp" "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. // 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) serviceID, err := trimID("service", serviceID)
if err != nil { if err != nil {
return nil, err return ServiceLogsResult{}, err
} }
query := url.Values{} query := url.Values{}
@@ -30,7 +50,7 @@ func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options Co
if options.Since != "" { if options.Since != "" {
ts, err := timestamp.GetTimestamp(options.Since, time.Now()) ts, err := timestamp.GetTimestamp(options.Since, time.Now())
if err != nil { 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) 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) resp, err := cli.get(ctx, "/services/"+serviceID+"/logs", query, nil)
if err != 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) { func TestServiceLogsError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) 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)) 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", Since: "2006-01-02TZ",
}) })
assert.Check(t, is.ErrorContains(err, `parsing time "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.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) 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.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -40,7 +40,7 @@ func TestServiceLogsError(t *testing.T) {
func TestServiceLogs(t *testing.T) { func TestServiceLogs(t *testing.T) {
const expectedURL = "/services/service_id/logs" const expectedURL = "/services/service_id/logs"
cases := []struct { cases := []struct {
options ContainerLogsOptions options ServiceLogsOptions
expectedQueryParams map[string]string expectedQueryParams map[string]string
expectedError string expectedError string
}{ }{
@@ -50,7 +50,7 @@ func TestServiceLogs(t *testing.T) {
}, },
}, },
{ {
options: ContainerLogsOptions{ options: ServiceLogsOptions{
Tail: "any", Tail: "any",
}, },
expectedQueryParams: map[string]string{ expectedQueryParams: map[string]string{
@@ -58,7 +58,7 @@ func TestServiceLogs(t *testing.T) {
}, },
}, },
{ {
options: ContainerLogsOptions{ options: ServiceLogsOptions{
ShowStdout: true, ShowStdout: true,
ShowStderr: true, ShowStderr: true,
Timestamps: true, Timestamps: true,
@@ -75,7 +75,7 @@ func TestServiceLogs(t *testing.T) {
}, },
}, },
{ {
options: ContainerLogsOptions{ options: ServiceLogsOptions{
// timestamp is passed as-is // timestamp is passed as-is
Since: "1136073600.000000001", Since: "1136073600.000000001",
}, },
@@ -85,7 +85,7 @@ func TestServiceLogs(t *testing.T) {
}, },
}, },
{ {
options: ContainerLogsOptions{ options: ServiceLogsOptions{
// invalid dates are not passed. // invalid dates are not passed.
Since: "invalid value", Since: "invalid value",
}, },
@@ -129,7 +129,7 @@ func ExampleClient_ServiceLogs_withTimeout() {
defer cancel() defer cancel()
client, _ := NewClientWithOpts(FromEnv) client, _ := NewClientWithOpts(FromEnv)
reader, err := client.ServiceLogs(ctx, "service_id", ContainerLogsOptions{}) reader, err := client.ServiceLogs(ctx, "service_id", ServiceLogsOptions{})
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -2,14 +2,24 @@ package client
import "context" 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. // 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) serviceID, err := trimID("service", serviceID)
if err != nil { if err != nil {
return err return ServiceRemoveResult{}, err
} }
resp, err := cli.delete(ctx, "/services/"+serviceID, nil, nil) resp, err := cli.delete(ctx, "/services/"+serviceID, nil, nil)
defer ensureReaderClosed(resp) 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"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) 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)) 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.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) 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.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) 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"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusNotFound, "no such service: service_id")))
assert.NilError(t, err) 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.ErrorContains(err, "no such service: service_id"))
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
} }
@@ -51,6 +51,6 @@ func TestServiceRemove(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
err = client.ServiceRemove(context.Background(), "service_id") _, err = client.ServiceRemove(context.Background(), "service_id", ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }

View File

@@ -10,18 +10,54 @@ import (
"github.com/moby/moby/api/types/swarm" "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 // ServiceUpdate updates a Service. The version number is required to avoid
// conflicting writes. It must be the value as set *before* the update. // 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 // You can find this value in the [swarm.Service.Meta] field, which can
// be found using [Client.ServiceInspectWithRaw]. // 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) serviceID, err := trimID("service", serviceID)
if err != nil { if err != nil {
return swarm.ServiceUpdateResponse{}, err return ServiceUpdateResult{}, err
} }
if err := validateServiceSpec(service); err != nil { if err := validateServiceSpec(service); err != nil {
return swarm.ServiceUpdateResponse{}, err return ServiceUpdateResult{}, err
} }
query := url.Values{} query := url.Values{}
@@ -36,21 +72,23 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
query.Set("version", version.String()) query.Set("version", version.String())
// ensure that the image is tagged // ensure that the image is tagged
var resolveWarning string var warnings []string
switch { switch {
case service.TaskTemplate.ContainerSpec != nil: case service.TaskTemplate.ContainerSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" { if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
service.TaskTemplate.ContainerSpec.Image = taggedImg service.TaskTemplate.ContainerSpec.Image = taggedImg
} }
if options.QueryRegistry { 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: case service.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" { if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
service.TaskTemplate.PluginSpec.Remote = taggedImg service.TaskTemplate.PluginSpec.Remote = taggedImg
} }
if options.QueryRegistry { 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) resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return swarm.ServiceUpdateResponse{}, err return ServiceUpdateResult{}, err
} }
var response swarm.ServiceUpdateResponse var response swarm.ServiceUpdateResponse
err = json.NewDecoder(resp.Body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
if resolveWarning != "" { warnings = append(warnings, response.Warnings...)
response.Warnings = append(response.Warnings, resolveWarning) return ServiceUpdateResult{Warnings: warnings}, err
}
return response, err
} }

View File

@@ -17,7 +17,9 @@ type SwarmJoinOptions struct {
} }
// SwarmJoinResult contains the result of joining a swarm. // 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. // SwarmJoin joins the swarm.
func (cli *Client) SwarmJoin(ctx context.Context, options SwarmJoinOptions) (SwarmJoinResult, error) { 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" "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. // TaskInspectResult contains the result of a task inspection.
type TaskInspectResult struct { type TaskInspectResult struct {
Task swarm.Task Task swarm.Task
@@ -13,7 +18,7 @@ type TaskInspectResult struct {
} }
// TaskInspect returns the task information and its raw representation. // 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) taskID, err := trimID("task", taskID)
if err != nil { if err != nil {
return TaskInspectResult{}, err return TaskInspectResult{}, err

View File

@@ -19,7 +19,7 @@ func TestTaskInspectError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) 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)) 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") return nil, errors.New("should not make request")
})) }))
assert.NilError(t, err) 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.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) 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.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty")) assert.Check(t, is.ErrorContains(err, "value is empty"))
} }
@@ -56,7 +56,7 @@ func TestTaskInspect(t *testing.T) {
})) }))
assert.NilError(t, err) 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.NilError(t, err)
assert.Check(t, is.Equal(result.Task.ID, "task_id")) 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. // TaskListResult contains the result of a task list operation.
type TaskListResult struct { type TaskListResult struct {
Tasks []swarm.Task Items []swarm.Task
} }
// TaskList returns the list of tasks. // TaskList returns the list of tasks.
@@ -32,5 +32,5 @@ func (cli *Client) TaskList(ctx context.Context, options TaskListOptions) (TaskL
var tasks []swarm.Task var tasks []swarm.Task
err = json.NewDecoder(resp.Body).Decode(&tasks) 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) result, err := client.TaskList(context.Background(), listCase.options)
assert.NilError(t, err) 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" "context"
"io" "io"
"net/url" "net/url"
"sync"
"time" "time"
"github.com/moby/moby/client/internal/timestamp" "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. // 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{} query := url.Values{}
if options.ShowStdout { if options.ShowStdout {
query.Set("stdout", "1") query.Set("stdout", "1")
@@ -24,7 +44,7 @@ func (cli *Client) TaskLogs(ctx context.Context, taskID string, options Containe
if options.Since != "" { if options.Since != "" {
ts, err := timestamp.GetTimestamp(options.Since, time.Now()) ts, err := timestamp.GetTimestamp(options.Since, time.Now())
if err != nil { if err != nil {
return nil, err return TaskLogsResult{}, err
} }
query.Set("since", ts) 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) resp, err := cli.get(ctx, "/tasks/"+taskID+"/logs", query, nil)
if err != 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()
} }

View File

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

View File

@@ -78,14 +78,14 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesCreate(c *testing.T) {
options := client.ServiceInspectOptions{InsertDefaults: true} options := client.ServiceInspectOptions{InsertDefaults: true}
// insertDefaults inserts UpdateConfig when service is fetched by ID // insertDefaults inserts UpdateConfig when service is fetched by ID
resp, _, err := apiClient.ServiceInspectWithRaw(ctx, id, options) res, err := apiClient.ServiceInspect(ctx, id, options)
out := fmt.Sprintf("%+v", resp) out := fmt.Sprintf("%+v", res.Service)
assert.NilError(c, err) assert.NilError(c, err)
assert.Assert(c, is.Contains(out, "UpdateConfig")) assert.Assert(c, is.Contains(out, "UpdateConfig"))
// insertDefaults inserts UpdateConfig when service is fetched by ID // insertDefaults inserts UpdateConfig when service is fetched by ID
resp, _, err = apiClient.ServiceInspectWithRaw(ctx, "top", options) res, err = apiClient.ServiceInspect(ctx, "top", options)
out = fmt.Sprintf("%+v", resp) out = fmt.Sprintf("%+v", res.Service)
assert.NilError(c, err) assert.NilError(c, err)
assert.Assert(c, is.Contains(out, "UpdateConfig")) assert.Assert(c, is.Contains(out, "UpdateConfig"))

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 { func GetRunningTasks(ctx context.Context, t *testing.T, c client.ServiceAPIClient, serviceID string) []swarmtypes.Task {
t.Helper() t.Helper()
result, err := c.TaskList(ctx, client.TaskListOptions{ taskList, err := c.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters). Filters: make(client.Filters).
Add("service", serviceID). Add("service", serviceID).
Add("desired-state", "running"), Add("desired-state", "running"),
}) })
assert.NilError(t, err) assert.NilError(t, err)
return result.Tasks return taskList.Items
} }
// ExecTask runs the passed in exec config on the given task // 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 // 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 { func NoTasksForService(ctx context.Context, apiClient client.ServiceAPIClient, serviceID string) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result { return func(log poll.LogT) poll.Result {
result, err := apiClient.TaskList(ctx, client.TaskListOptions{ taskList, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID), Filters: make(client.Filters).Add("service", serviceID),
}) })
if err == nil { if err == nil {
if len(result.Tasks) == 0 { if len(taskList.Items) == 0 {
return poll.Success() return poll.Success()
} }
if len(result.Tasks) > 0 { if len(taskList.Items) > 0 {
return poll.Continue("task count for service %s at %d waiting for 0", serviceID, len(result.Tasks)) return poll.Continue("task count for service %s at %d waiting for 0", serviceID, len(taskList.Items))
} }
return poll.Continue("waiting for tasks for service %s to be deleted", serviceID) 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 // NoTasks verifies that all tasks are gone
func NoTasks(ctx context.Context, apiClient client.ServiceAPIClient) func(log poll.LogT) poll.Result { func NoTasks(ctx context.Context, apiClient client.ServiceAPIClient) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result { return func(log poll.LogT) poll.Result {
result, err := apiClient.TaskList(ctx, client.TaskListOptions{}) taskResult, err := apiClient.TaskList(ctx, client.TaskListOptions{})
switch { switch {
case err != nil: case err != nil:
return poll.Error(err) return poll.Error(err)
case len(result.Tasks) == 0: case len(taskResult.Items) == 0:
return poll.Success() return poll.Success()
default: default:
return poll.Continue("waiting for all tasks to be removed: task count at %d", len(result.Tasks)) return poll.Continue("waiting for all tasks to be removed: task count at %d", len(taskResult.Items))
} }
} }
} }
@@ -47,12 +47,12 @@ func NoTasks(ctx context.Context, apiClient client.ServiceAPIClient) func(log po
// RunningTasksCount verifies there are `instances` tasks running for `serviceID` // 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 { 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 { return func(log poll.LogT) poll.Result {
result, err := apiClient.TaskList(ctx, client.TaskListOptions{ taskList, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID), Filters: make(client.Filters).Add("service", serviceID),
}) })
var running int var running int
var taskError string var taskError string
for _, task := range result.Tasks { for _, task := range taskList.Items {
switch task.Status.State { switch task.Status.State {
case swarmtypes.TaskStateRunning: case swarmtypes.TaskStateRunning:
running++ running++
@@ -76,7 +76,7 @@ func RunningTasksCount(ctx context.Context, apiClient client.ServiceAPIClient, s
case running == int(instances): case running == int(instances):
return poll.Success() return poll.Success()
default: default:
return poll.Continue("running task count at %d waiting for %d (total tasks: %d)", running, instances, len(result.Tasks)) return poll.Continue("running task count at %d waiting for %d (total tasks: %d)", running, instances, len(taskList.Items))
} }
} }
} }
@@ -97,7 +97,7 @@ func JobComplete(ctx context.Context, apiClient client.ServiceAPIClient, service
previousResult := "" previousResult := ""
return func(log poll.LogT) poll.Result { return func(log poll.LogT) poll.Result {
result, err := apiClient.TaskList(ctx, client.TaskListOptions{ taskList, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: filter, Filters: filter,
}) })
if err != nil { if err != nil {
@@ -110,7 +110,7 @@ func JobComplete(ctx context.Context, apiClient client.ServiceAPIClient, service
var runningSlot []int var runningSlot []int
var runningID []string var runningID []string
for _, task := range result.Tasks { for _, task := range taskList.Items {
// make sure the task has the same job iteration // make sure the task has the same job iteration
if task.JobIteration == nil || task.JobIteration.Index != jobIteration.Index { if task.JobIteration == nil || task.JobIteration.Index != jobIteration.Index {
continue continue

View File

@@ -76,7 +76,8 @@ func TestInspectNetwork(t *testing.T) {
swarm.ServiceWithNetwork(networkName), swarm.ServiceWithNetwork(networkName),
) )
defer func() { defer func() {
assert.NilError(t, c1.ServiceRemove(ctx, serviceID)) _, err := c1.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err)
poll.WaitOn(t, swarm.NoTasksForService(ctx, c1, serviceID), swarm.ServicePoll) poll.WaitOn(t, swarm.NoTasksForService(ctx, c1, serviceID), swarm.ServicePoll)
}() }()

View File

@@ -85,7 +85,7 @@ func TestHostPortMappings(t *testing.T) {
{Protocol: networktypes.TCP, TargetPort: 80, PublishedPort: 80, PublishMode: swarmtypes.PortConfigPublishModeHost}, {Protocol: networktypes.TCP, TargetPort: 80, PublishedPort: 80, PublishMode: swarmtypes.PortConfigPublishModeHost},
}, },
})) }))
defer apiClient.ServiceRemove(ctx, svcID) defer apiClient.ServiceRemove(ctx, svcID, client.ServiceRemoveOptions{})
poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, svcID, 1), swarm.ServicePoll) poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, svcID, 1), swarm.ServicePoll)

View File

@@ -245,10 +245,10 @@ func TestServiceWithPredefinedNetwork(t *testing.T) {
poll.WaitOn(t, swarm.RunningTasksCount(ctx, c, serviceID, instances), swarm.ServicePoll) poll.WaitOn(t, swarm.RunningTasksCount(ctx, c, serviceID, instances), swarm.ServicePoll)
_, _, err := c.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) _, err := c.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
err = c.ServiceRemove(ctx, serviceID) _, err = c.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }
@@ -286,10 +286,10 @@ func TestServiceRemoveKeepsIngressNetwork(t *testing.T) {
poll.WaitOn(t, swarm.RunningTasksCount(ctx, c, serviceID, instances), swarm.ServicePoll) poll.WaitOn(t, swarm.RunningTasksCount(ctx, c, serviceID, instances), swarm.ServicePoll)
_, _, err := c.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) _, err := c.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
err = c.ServiceRemove(ctx, serviceID) _, err = c.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
poll.WaitOn(t, noServices(ctx, c), swarm.ServicePoll) poll.WaitOn(t, noServices(ctx, c), swarm.ServicePoll)
@@ -333,14 +333,14 @@ func swarmIngressReady(ctx context.Context, apiClient client.NetworkAPIClient) f
func noServices(ctx context.Context, apiClient client.ServiceAPIClient) func(log poll.LogT) poll.Result { func noServices(ctx context.Context, apiClient client.ServiceAPIClient) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result { return func(log poll.LogT) poll.Result {
services, err := apiClient.ServiceList(ctx, client.ServiceListOptions{}) result, err := apiClient.ServiceList(ctx, client.ServiceListOptions{})
switch { switch {
case err != nil: case err != nil:
return poll.Error(err) return poll.Error(err)
case len(services) == 0: case len(result.Services) == 0:
return poll.Success() return poll.Success()
default: default:
return poll.Continue("waiting for all services to be removed: service count at %d", len(services)) return poll.Continue("waiting for all services to be removed: service count at %d", len(result.Services))
} }
} }
} }
@@ -369,7 +369,7 @@ func TestServiceWithDataPathPortInit(t *testing.T) {
info := d.Info(t) info := d.Info(t)
assert.Equal(t, info.Swarm.Cluster.DataPathPort, datapathPort) assert.Equal(t, info.Swarm.Cluster.DataPathPort, datapathPort)
err := c.ServiceRemove(ctx, serviceID) _, err := c.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
poll.WaitOn(t, noServices(ctx, c), swarm.ServicePoll) poll.WaitOn(t, noServices(ctx, c), swarm.ServicePoll)
poll.WaitOn(t, swarm.NoTasks(ctx, c), swarm.ServicePoll) poll.WaitOn(t, swarm.NoTasks(ctx, c), swarm.ServicePoll)
@@ -402,7 +402,7 @@ func TestServiceWithDataPathPortInit(t *testing.T) {
info = d.Info(t) info = d.Info(t)
var defaultDataPathPort uint32 = 4789 var defaultDataPathPort uint32 = 4789
assert.Equal(t, info.Swarm.Cluster.DataPathPort, defaultDataPathPort) assert.Equal(t, info.Swarm.Cluster.DataPathPort, defaultDataPathPort)
err = nc.ServiceRemove(ctx, serviceID) _, err = nc.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
poll.WaitOn(t, noServices(ctx, nc), swarm.ServicePoll) poll.WaitOn(t, noServices(ctx, nc), swarm.ServicePoll)
poll.WaitOn(t, swarm.NoTasks(ctx, nc), swarm.ServicePoll) poll.WaitOn(t, swarm.NoTasks(ctx, nc), swarm.ServicePoll)
@@ -440,7 +440,7 @@ func TestServiceWithDefaultAddressPoolInit(t *testing.T) {
poll.WaitOn(t, swarm.RunningTasksCount(ctx, cli, serviceID, instances), swarm.ServicePoll) poll.WaitOn(t, swarm.RunningTasksCount(ctx, cli, serviceID, instances), swarm.ServicePoll)
_, _, err := cli.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) _, err := cli.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
res, err := cli.NetworkInspect(ctx, overlayID, client.NetworkInspectOptions{Verbose: true}) res, err := cli.NetworkInspect(ctx, overlayID, client.NetworkInspectOptions{Verbose: true})
@@ -459,7 +459,7 @@ func TestServiceWithDefaultAddressPoolInit(t *testing.T) {
assert.Assert(t, len(res.Network.IPAM.Config) > 0) assert.Assert(t, len(res.Network.IPAM.Config) > 0)
assert.Equal(t, res.Network.IPAM.Config[0].Subnet, netip.MustParsePrefix("20.20.0.0/24")) assert.Equal(t, res.Network.IPAM.Config[0].Subnet, netip.MustParsePrefix("20.20.0.0/24"))
err = cli.ServiceRemove(ctx, serviceID) _, err = cli.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
poll.WaitOn(t, noServices(ctx, cli), swarm.ServicePoll) poll.WaitOn(t, noServices(ctx, cli), swarm.ServicePoll)
poll.WaitOn(t, swarm.NoTasks(ctx, cli), swarm.ServicePoll) poll.WaitOn(t, swarm.NoTasks(ctx, cli), swarm.ServicePoll)
assert.NilError(t, err) assert.NilError(t, err)

View File

@@ -100,10 +100,10 @@ func TestCreateServiceMultipleTimes(t *testing.T) {
serviceID := swarm.CreateService(ctx, t, d, serviceSpec...) serviceID := swarm.CreateService(ctx, t, d, serviceSpec...)
poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID, instances), swarm.ServicePoll) poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID, instances), swarm.ServicePoll)
_, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) _, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
err = apiClient.ServiceRemove(ctx, serviceID) _, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
poll.WaitOn(t, swarm.NoTasksForService(ctx, apiClient, serviceID), swarm.ServicePoll) poll.WaitOn(t, swarm.NoTasksForService(ctx, apiClient, serviceID), swarm.ServicePoll)
@@ -111,7 +111,7 @@ func TestCreateServiceMultipleTimes(t *testing.T) {
serviceID2 := swarm.CreateService(ctx, t, d, serviceSpec...) serviceID2 := swarm.CreateService(ctx, t, d, serviceSpec...)
poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID2, instances), swarm.ServicePoll) poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID2, instances), swarm.ServicePoll)
err = apiClient.ServiceRemove(ctx, serviceID2) _, err = apiClient.ServiceRemove(ctx, serviceID2, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
// we can't just wait on no tasks for the service, counter-intuitively. // we can't just wait on no tasks for the service, counter-intuitively.
@@ -185,7 +185,7 @@ func TestCreateServiceMaxReplicas(t *testing.T) {
serviceID := swarm.CreateService(ctx, t, d, serviceSpec...) serviceID := swarm.CreateService(ctx, t, d, serviceSpec...)
poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID, maxReplicas), swarm.ServicePoll) poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID, maxReplicas), swarm.ServicePoll)
_, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) _, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }
@@ -227,7 +227,7 @@ func TestCreateServiceSecretFileMode(t *testing.T) {
poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID, instances), swarm.ServicePoll) poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID, instances), swarm.ServicePoll)
body, err := apiClient.ServiceLogs(ctx, serviceID, client.ContainerLogsOptions{ body, err := apiClient.ServiceLogs(ctx, serviceID, client.ServiceLogsOptions{
Tail: "1", Tail: "1",
ShowStdout: true, ShowStdout: true,
}) })
@@ -238,7 +238,7 @@ func TestCreateServiceSecretFileMode(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Contains(string(content), "-rwxrwxrwx")) assert.Check(t, is.Contains(string(content), "-rwxrwxrwx"))
err = apiClient.ServiceRemove(ctx, serviceID) _, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
poll.WaitOn(t, swarm.NoTasksForService(ctx, apiClient, serviceID), swarm.ServicePoll) poll.WaitOn(t, swarm.NoTasksForService(ctx, apiClient, serviceID), swarm.ServicePoll)
@@ -286,7 +286,7 @@ func TestCreateServiceConfigFileMode(t *testing.T) {
poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID, instances)) poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, serviceID, instances))
body, err := apiClient.ServiceLogs(ctx, serviceID, client.ContainerLogsOptions{ body, err := apiClient.ServiceLogs(ctx, serviceID, client.ServiceLogsOptions{
Tail: "1", Tail: "1",
ShowStdout: true, ShowStdout: true,
}) })
@@ -297,7 +297,7 @@ func TestCreateServiceConfigFileMode(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Contains(string(content), "-rwxrwxrwx")) assert.Check(t, is.Contains(string(content), "-rwxrwxrwx"))
err = apiClient.ServiceRemove(ctx, serviceID) _, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
poll.WaitOn(t, swarm.NoTasksForService(ctx, apiClient, serviceID)) poll.WaitOn(t, swarm.NoTasksForService(ctx, apiClient, serviceID))
@@ -368,25 +368,25 @@ func TestCreateServiceSysctls(t *testing.T) {
// more complex) // more complex)
// get all tasks of the service, so we can get the container // get all tasks of the service, so we can get the container
taskResult, err := apiClient.TaskList(ctx, client.TaskListOptions{ taskList, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID), Filters: make(client.Filters).Add("service", serviceID),
}) })
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(len(taskResult.Tasks), 1)) assert.Check(t, is.Equal(len(taskList.Items), 1))
// verify that the container has the sysctl option set // verify that the container has the sysctl option set
ctnr, err := apiClient.ContainerInspect(ctx, taskResult.Tasks[0].Status.ContainerStatus.ContainerID) ctnr, err := apiClient.ContainerInspect(ctx, taskList.Items[0].Status.ContainerStatus.ContainerID)
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, ctnr.HostConfig.Sysctls, expectedSysctls) assert.DeepEqual(t, ctnr.HostConfig.Sysctls, expectedSysctls)
// verify that the task has the sysctl option set in the task object // verify that the task has the sysctl option set in the task object
assert.DeepEqual(t, taskResult.Tasks[0].Spec.ContainerSpec.Sysctls, expectedSysctls) assert.DeepEqual(t, taskList.Items[0].Spec.ContainerSpec.Sysctls, expectedSysctls)
// verify that the service also has the sysctl set in the spec. // verify that the service also has the sysctl set in the spec.
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, assert.DeepEqual(t,
service.Spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls, result.Service.Spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls,
) )
} }
} }
@@ -438,25 +438,25 @@ func TestCreateServiceCapabilities(t *testing.T) {
// level has been tested elsewhere. // level has been tested elsewhere.
// get all tasks of the service, so we can get the container // get all tasks of the service, so we can get the container
taskResult, err := apiClient.TaskList(ctx, client.TaskListOptions{ taskList, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID), Filters: make(client.Filters).Add("service", serviceID),
}) })
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Equal(len(taskResult.Tasks), 1)) assert.Check(t, is.Equal(len(taskList.Items), 1))
// verify that the container has the capabilities option set // verify that the container has the capabilities option set
ctnr, err := apiClient.ContainerInspect(ctx, taskResult.Tasks[0].Status.ContainerStatus.ContainerID) ctnr, err := apiClient.ContainerInspect(ctx, taskList.Items[0].Status.ContainerStatus.ContainerID)
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, ctnr.HostConfig.CapAdd, capAdd) assert.DeepEqual(t, ctnr.HostConfig.CapAdd, capAdd)
assert.DeepEqual(t, ctnr.HostConfig.CapDrop, capDrop) assert.DeepEqual(t, ctnr.HostConfig.CapDrop, capDrop)
// verify that the task has the capabilities option set in the task object // verify that the task has the capabilities option set in the task object
assert.DeepEqual(t, taskResult.Tasks[0].Spec.ContainerSpec.CapabilityAdd, capAdd) assert.DeepEqual(t, taskList.Items[0].Spec.ContainerSpec.CapabilityAdd, capAdd)
assert.DeepEqual(t, taskResult.Tasks[0].Spec.ContainerSpec.CapabilityDrop, capDrop) assert.DeepEqual(t, taskList.Items[0].Spec.ContainerSpec.CapabilityDrop, capDrop)
// verify that the service also has the capabilities set in the spec. // verify that the service also has the capabilities set in the spec.
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, service.Spec.TaskTemplate.ContainerSpec.CapabilityAdd, capAdd) assert.DeepEqual(t, result.Service.Spec.TaskTemplate.ContainerSpec.CapabilityAdd, capAdd)
assert.DeepEqual(t, service.Spec.TaskTemplate.ContainerSpec.CapabilityDrop, capDrop) assert.DeepEqual(t, result.Service.Spec.TaskTemplate.ContainerSpec.CapabilityDrop, capDrop)
} }

View File

@@ -38,7 +38,7 @@ func TestInspect(t *testing.T) {
id := resp.ID id := resp.ID
poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, id, instances)) poll.WaitOn(t, swarm.RunningTasksCount(ctx, apiClient, id, instances))
service, _, err := apiClient.ServiceInspectWithRaw(ctx, id, client.ServiceInspectOptions{}) result, err := apiClient.ServiceInspect(ctx, id, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
expected := swarmtypes.Service{ expected := swarmtypes.Service{
@@ -50,7 +50,7 @@ func TestInspect(t *testing.T) {
UpdatedAt: now, UpdatedAt: now,
}, },
} }
assert.Check(t, is.DeepEqual(service, expected, cmpServiceOpts())) assert.Check(t, is.DeepEqual(result.Service, expected, cmpServiceOpts()))
} }
// TODO: use helpers from gotest.tools/assert/opt when available // TODO: use helpers from gotest.tools/assert/opt when available

View File

@@ -74,12 +74,12 @@ func TestReplicatedJob(t *testing.T) {
swarm.ServiceWithCommand([]string{"true"}), swarm.ServiceWithCommand([]string{"true"}),
) )
service, _, err := apiClient.ServiceInspectWithRaw( result, err := apiClient.ServiceInspect(
ctx, id, client.ServiceInspectOptions{}, ctx, id, client.ServiceInspectOptions{},
) )
assert.NilError(t, err) assert.NilError(t, err)
poll.WaitOn(t, swarm.JobComplete(ctx, apiClient, service), swarm.ServicePoll) poll.WaitOn(t, swarm.JobComplete(ctx, apiClient, result.Service), swarm.ServicePoll)
} }
// TestUpdateReplicatedJob tests that a job can be updated, and that it runs with the // TestUpdateReplicatedJob tests that a job can be updated, and that it runs with the
@@ -107,33 +107,33 @@ func TestUpdateReplicatedJob(t *testing.T) {
swarm.ServiceWithCommand([]string{"true"}), swarm.ServiceWithCommand([]string{"true"}),
) )
service, _, err := apiClient.ServiceInspectWithRaw( result, err := apiClient.ServiceInspect(
ctx, id, client.ServiceInspectOptions{}, ctx, id, client.ServiceInspectOptions{},
) )
assert.NilError(t, err) assert.NilError(t, err)
// wait for the job to completed // wait for the job to completed
poll.WaitOn(t, swarm.JobComplete(ctx, apiClient, service), swarm.ServicePoll) poll.WaitOn(t, swarm.JobComplete(ctx, apiClient, result.Service), swarm.ServicePoll)
// update the job. // update the job.
spec := service.Spec spec := result.Service.Spec
spec.TaskTemplate.ForceUpdate++ spec.TaskTemplate.ForceUpdate++
_, err = apiClient.ServiceUpdate( _, err = apiClient.ServiceUpdate(
ctx, id, service.Version, spec, client.ServiceUpdateOptions{}, ctx, id, result.Service.Version, spec, client.ServiceUpdateOptions{},
) )
assert.NilError(t, err) assert.NilError(t, err)
service2, _, err := apiClient.ServiceInspectWithRaw( result2, err := apiClient.ServiceInspect(
ctx, id, client.ServiceInspectOptions{}, ctx, id, client.ServiceInspectOptions{},
) )
assert.NilError(t, err) assert.NilError(t, err)
// assert that the job iteration has increased // assert that the job iteration has increased
assert.Assert(t, assert.Assert(t,
service.JobStatus.JobIteration.Index < service2.JobStatus.JobIteration.Index, result.Service.JobStatus.JobIteration.Index < result2.Service.JobStatus.JobIteration.Index,
) )
// now wait for the service to complete a second time. // now wait for the service to complete a second time.
poll.WaitOn(t, swarm.JobComplete(ctx, apiClient, service2), swarm.ServicePoll) poll.WaitOn(t, swarm.JobComplete(ctx, apiClient, result2.Service), swarm.ServicePoll)
} }

View File

@@ -52,12 +52,12 @@ func TestServiceListWithStatuses(t *testing.T) {
// serviceContainerCount function does not do. instead, we'll use a // serviceContainerCount function does not do. instead, we'll use a
// bespoke closure right here. // bespoke closure right here.
poll.WaitOn(t, func(log poll.LogT) poll.Result { poll.WaitOn(t, func(log poll.LogT) poll.Result {
taskResult, err := apiClient.TaskList(ctx, client.TaskListOptions{ taskList, err := apiClient.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", id), Filters: make(client.Filters).Add("service", id),
}) })
running := 0 running := 0
for _, task := range taskResult.Tasks { for _, task := range taskList.Items {
if task.Status.State == swarmtypes.TaskStateRunning { if task.Status.State == swarmtypes.TaskStateRunning {
running++ running++
} }
@@ -71,25 +71,25 @@ func TestServiceListWithStatuses(t *testing.T) {
default: default:
return poll.Continue( return poll.Continue(
"running task count %d (%d total), waiting for %d", "running task count %d (%d total), waiting for %d",
running, len(taskResult.Tasks), i+1, running, len(taskList.Items), i+1,
) )
} }
}) })
} }
// now, let's do the list operation with no status arg set. // now, let's do the list operation with no status arg set.
resp, err := apiClient.ServiceList(ctx, client.ServiceListOptions{}) result, err := apiClient.ServiceList(ctx, client.ServiceListOptions{})
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Len(resp, serviceCount)) assert.Check(t, is.Len(result.Services, serviceCount))
for _, service := range resp { for _, service := range result.Services {
assert.Check(t, is.Nil(service.ServiceStatus)) assert.Check(t, is.Nil(service.ServiceStatus))
} }
// now try again, but with Status: true. This time, we should have statuses // now try again, but with Status: true. This time, we should have statuses
resp, err = apiClient.ServiceList(ctx, client.ServiceListOptions{Status: true}) result, err = apiClient.ServiceList(ctx, client.ServiceListOptions{Status: true})
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Len(resp, serviceCount)) assert.Check(t, is.Len(result.Services, serviceCount))
for _, service := range resp { for _, service := range result.Services {
replicas := *service.Spec.Mode.Replicated.Replicas replicas := *service.Spec.Mode.Replicated.Replicas
assert.Assert(t, service.ServiceStatus != nil) assert.Assert(t, service.ServiceStatus != nil)

View File

@@ -171,7 +171,7 @@ func TestSwarmScopedNetFromConfig(t *testing.T) {
swarm.ServiceWithNetwork(swarmNetName), swarm.ServiceWithNetwork(swarmNetName),
) )
defer func() { defer func() {
err := c.ServiceRemove(ctx, serviceID) _, err := c.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
}() }()
@@ -246,7 +246,7 @@ func TestDockerIngressChainPosition(t *testing.T) {
}), }),
) )
defer func() { defer func() {
err := c.ServiceRemove(ctx, serviceID) _, err := c.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
}() }()
@@ -306,7 +306,7 @@ func TestRestoreIngressRulesOnFirewalldReload(t *testing.T) {
}), }),
) )
defer func() { defer func() {
err := c.ServiceRemove(ctx, serviceID) _, err := c.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
}() }()

View File

@@ -68,7 +68,7 @@ func TestServiceUpdateLabel(t *testing.T) {
service = getService(ctx, t, apiClient, serviceID) service = getService(ctx, t, apiClient, serviceID)
assert.Check(t, is.DeepEqual(service.Spec.Labels, map[string]string{"foo": "bar"})) assert.Check(t, is.DeepEqual(service.Spec.Labels, map[string]string{"foo": "bar"}))
err = apiClient.ServiceRemove(ctx, serviceID) _, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }
@@ -130,7 +130,7 @@ func TestServiceUpdateSecrets(t *testing.T) {
service = getService(ctx, t, apiClient, serviceID) service = getService(ctx, t, apiClient, serviceID)
assert.Check(t, is.Equal(0, len(service.Spec.TaskTemplate.ContainerSpec.Secrets))) assert.Check(t, is.Equal(0, len(service.Spec.TaskTemplate.ContainerSpec.Secrets)))
err = apiClient.ServiceRemove(ctx, serviceID) _, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }
@@ -194,7 +194,7 @@ func TestServiceUpdateConfigs(t *testing.T) {
service = getService(ctx, t, apiClient, serviceID) service = getService(ctx, t, apiClient, serviceID)
assert.Check(t, is.Equal(0, len(service.Spec.TaskTemplate.ContainerSpec.Configs))) assert.Check(t, is.Equal(0, len(service.Spec.TaskTemplate.ContainerSpec.Configs)))
err = apiClient.ServiceRemove(ctx, serviceID) _, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }
@@ -246,7 +246,7 @@ func TestServiceUpdateNetwork(t *testing.T) {
err = apiClient.NetworkRemove(ctx, overlayID) err = apiClient.NetworkRemove(ctx, overlayID)
assert.NilError(t, err) assert.NilError(t, err)
err = apiClient.ServiceRemove(ctx, serviceID) _, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }
@@ -319,19 +319,19 @@ func TestServiceUpdatePidsLimit(t *testing.T) {
}) })
} }
err := apiClient.ServiceRemove(ctx, serviceID) _, err := apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }
func getServiceTaskContainer(ctx context.Context, t *testing.T, cli client.APIClient, serviceID string) container.InspectResponse { func getServiceTaskContainer(ctx context.Context, t *testing.T, cli client.APIClient, serviceID string) container.InspectResponse {
t.Helper() t.Helper()
taskResult, err := cli.TaskList(ctx, client.TaskListOptions{ taskList, err := cli.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).Add("service", serviceID).Add("desired-state", "running"), Filters: make(client.Filters).Add("service", serviceID).Add("desired-state", "running"),
}) })
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, len(taskResult.Tasks) > 0) assert.Assert(t, len(taskList.Items) > 0)
ctr, err := cli.ContainerInspect(ctx, taskResult.Tasks[0].Status.ContainerStatus.ContainerID) ctr, err := cli.ContainerInspect(ctx, taskList.Items[0].Status.ContainerStatus.ContainerID)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, ctr.State.Running, true) assert.Equal(t, ctr.State.Running, true)
return ctr return ctr
@@ -339,17 +339,19 @@ func getServiceTaskContainer(ctx context.Context, t *testing.T, cli client.APICl
func getService(ctx context.Context, t *testing.T, apiClient client.ServiceAPIClient, serviceID string) swarmtypes.Service { func getService(ctx context.Context, t *testing.T, apiClient client.ServiceAPIClient, serviceID string) swarmtypes.Service {
t.Helper() t.Helper()
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
return service return result.Service
} }
func serviceIsUpdated(ctx context.Context, apiClient client.ServiceAPIClient, serviceID string) func(log poll.LogT) poll.Result { func serviceIsUpdated(ctx context.Context, apiClient client.ServiceAPIClient, serviceID string) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result { return func(log poll.LogT) poll.Result {
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
switch { if err != nil {
case err != nil:
return poll.Error(err) return poll.Error(err)
}
service := result.Service
switch {
case service.UpdateStatus != nil && service.UpdateStatus.State == swarmtypes.UpdateStateCompleted: case service.UpdateStatus != nil && service.UpdateStatus.State == swarmtypes.UpdateStateCompleted:
return poll.Success() return poll.Success()
default: default:
@@ -363,11 +365,11 @@ func serviceIsUpdated(ctx context.Context, apiClient client.ServiceAPIClient, se
func serviceSpecIsUpdated(ctx context.Context, apiClient client.ServiceAPIClient, serviceID string, serviceOldVersion uint64) func(log poll.LogT) poll.Result { func serviceSpecIsUpdated(ctx context.Context, apiClient client.ServiceAPIClient, serviceID string, serviceOldVersion uint64) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result { return func(log poll.LogT) poll.Result {
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{}) result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
switch { switch {
case err != nil: case err != nil:
return poll.Error(err) return poll.Error(err)
case service.Version.Index > serviceOldVersion: case result.Service.Version.Index > serviceOldVersion:
return poll.Success() return poll.Success()
default: default:
return poll.Continue("waiting for service %s to be updated", serviceID) return poll.Continue("waiting for service %s to be updated", serviceID)

View File

@@ -45,9 +45,9 @@ func (d *Daemon) GetService(ctx context.Context, t testing.TB, id string) *swarm
cli := d.NewClientT(t) cli := d.NewClientT(t)
defer cli.Close() defer cli.Close()
service, _, err := cli.ServiceInspectWithRaw(ctx, id, client.ServiceInspectOptions{}) res, err := cli.ServiceInspect(ctx, id, client.ServiceInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
return &service return &res.Service
} }
// GetServiceTasks returns the swarm tasks for the specified service // GetServiceTasks returns the swarm tasks for the specified service
@@ -70,9 +70,9 @@ func (d *Daemon) GetServiceTasksWithFilters(ctx context.Context, t testing.TB, s
Filters: filterArgs, Filters: filterArgs,
} }
result, err := cli.TaskList(ctx, options) taskList, err := cli.TaskList(ctx, options)
assert.NilError(t, err) assert.NilError(t, err)
return result.Tasks return taskList.Items
} }
// UpdateService updates a swarm service with the specified service constructor // UpdateService updates a swarm service with the specified service constructor
@@ -95,7 +95,7 @@ func (d *Daemon) RemoveService(ctx context.Context, t testing.TB, id string) {
cli := d.NewClientT(t) cli := d.NewClientT(t)
defer cli.Close() defer cli.Close()
err := cli.ServiceRemove(ctx, id) _, err := cli.ServiceRemove(ctx, id, client.ServiceRemoveOptions{})
assert.NilError(t, err) assert.NilError(t, err)
} }
@@ -105,9 +105,9 @@ func (d *Daemon) ListServices(ctx context.Context, t testing.TB) []swarm.Service
cli := d.NewClientT(t) cli := d.NewClientT(t)
defer cli.Close() defer cli.Close()
services, err := cli.ServiceList(ctx, client.ServiceListOptions{}) res, err := cli.ServiceList(ctx, client.ServiceListOptions{})
assert.NilError(t, err) assert.NilError(t, err)
return services return res.Services
} }
// GetTask returns the swarm task identified by the specified id // GetTask returns the swarm task identified by the specified id
@@ -116,7 +116,7 @@ func (d *Daemon) GetTask(ctx context.Context, t testing.TB, id string) swarm.Tas
cli := d.NewClientT(t) cli := d.NewClientT(t)
defer cli.Close() defer cli.Close()
result, err := cli.TaskInspect(ctx, id) result, err := cli.TaskInspect(ctx, id, client.TaskInspectOptions{})
assert.NilError(t, err) assert.NilError(t, err)
return result.Task return result.Task
} }

View File

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

View File

@@ -14,35 +14,59 @@ import (
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
) )
// ServiceCreate creates a new service. // ServiceCreateOptions contains the options to use when creating a service.
func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options ServiceCreateOptions) (swarm.ServiceCreateResponse, error) { type ServiceCreateOptions struct {
var response swarm.ServiceCreateResponse // 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 // 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) { if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) {
service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{} service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
} }
if err := validateServiceSpec(service); err != nil { if err := validateServiceSpec(service); err != nil {
return response, err return ServiceCreateResult{}, err
} }
// ensure that the image is tagged // ensure that the image is tagged
var resolveWarning string var warnings []string
switch { switch {
case service.TaskTemplate.ContainerSpec != nil: case service.TaskTemplate.ContainerSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" { if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
service.TaskTemplate.ContainerSpec.Image = taggedImg service.TaskTemplate.ContainerSpec.Image = taggedImg
} }
if options.QueryRegistry { 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: case service.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" { if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
service.TaskTemplate.PluginSpec.Remote = taggedImg service.TaskTemplate.PluginSpec.Remote = taggedImg
} }
if options.QueryRegistry { 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) resp, err := cli.post(ctx, "/services/create", nil, service, headers)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return response, err return ServiceCreateResult{}, err
} }
var response swarm.ServiceCreateResponse
err = json.NewDecoder(resp.Body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
if resolveWarning != "" { warnings = append(warnings, response.Warnings...)
response.Warnings = append(response.Warnings, resolveWarning)
}
return response, err return ServiceCreateResult{
ID: response.ID,
Warnings: warnings,
}, err
} }
func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {

View File

@@ -1,38 +1,40 @@
package client package client
import ( import (
"bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io"
"net/url" "net/url"
"github.com/moby/moby/api/types/swarm" "github.com/moby/moby/api/types/swarm"
) )
// ServiceInspectWithRaw returns the service information and the raw data. // ServiceInspectOptions holds parameters related to the service inspect operation.
func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string, opts ServiceInspectOptions) (swarm.Service, []byte, error) { 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) serviceID, err := trimID("service", serviceID)
if err != nil { if err != nil {
return swarm.Service{}, nil, err return ServiceInspectResult{}, err
} }
query := url.Values{} 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) resp, err := cli.get(ctx, "/services/"+serviceID, query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return swarm.Service{}, nil, err return ServiceInspectResult{}, err
} }
body, err := io.ReadAll(resp.Body) var out ServiceInspectResult
if err != nil { out.Raw, err = decodeWithRaw(resp, &out.Service)
return swarm.Service{}, nil, err return out, err
}
var response swarm.Service
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
} }

View File

@@ -8,8 +8,22 @@ import (
"github.com/moby/moby/api/types/swarm" "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. // 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{} query := url.Values{}
options.Filters.updateURLValues(query) 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) resp, err := cli.get(ctx, "/services", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return nil, err return ServiceListResult{}, err
} }
var services []swarm.Service var services []swarm.Service
err = json.NewDecoder(resp.Body).Decode(&services) err = json.NewDecoder(resp.Body).Decode(&services)
return services, err return ServiceListResult{Services: services}, err
} }

View File

@@ -5,17 +5,37 @@ import (
"fmt" "fmt"
"io" "io"
"net/url" "net/url"
"sync"
"time" "time"
"github.com/moby/moby/client/internal/timestamp" "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. // 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) serviceID, err := trimID("service", serviceID)
if err != nil { if err != nil {
return nil, err return ServiceLogsResult{}, err
} }
query := url.Values{} query := url.Values{}
@@ -30,7 +50,7 @@ func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options Co
if options.Since != "" { if options.Since != "" {
ts, err := timestamp.GetTimestamp(options.Since, time.Now()) ts, err := timestamp.GetTimestamp(options.Since, time.Now())
if err != nil { 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) 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) resp, err := cli.get(ctx, "/services/"+serviceID+"/logs", query, nil)
if err != 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

@@ -2,14 +2,24 @@ package client
import "context" 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. // 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) serviceID, err := trimID("service", serviceID)
if err != nil { if err != nil {
return err return ServiceRemoveResult{}, err
} }
resp, err := cli.delete(ctx, "/services/"+serviceID, nil, nil) resp, err := cli.delete(ctx, "/services/"+serviceID, nil, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
return err return ServiceRemoveResult{}, err
} }

View File

@@ -10,18 +10,54 @@ import (
"github.com/moby/moby/api/types/swarm" "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 // ServiceUpdate updates a Service. The version number is required to avoid
// conflicting writes. It must be the value as set *before* the update. // 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 // You can find this value in the [swarm.Service.Meta] field, which can
// be found using [Client.ServiceInspectWithRaw]. // 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) serviceID, err := trimID("service", serviceID)
if err != nil { if err != nil {
return swarm.ServiceUpdateResponse{}, err return ServiceUpdateResult{}, err
} }
if err := validateServiceSpec(service); err != nil { if err := validateServiceSpec(service); err != nil {
return swarm.ServiceUpdateResponse{}, err return ServiceUpdateResult{}, err
} }
query := url.Values{} query := url.Values{}
@@ -36,21 +72,23 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
query.Set("version", version.String()) query.Set("version", version.String())
// ensure that the image is tagged // ensure that the image is tagged
var resolveWarning string var warnings []string
switch { switch {
case service.TaskTemplate.ContainerSpec != nil: case service.TaskTemplate.ContainerSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" { if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
service.TaskTemplate.ContainerSpec.Image = taggedImg service.TaskTemplate.ContainerSpec.Image = taggedImg
} }
if options.QueryRegistry { 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: case service.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" { if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
service.TaskTemplate.PluginSpec.Remote = taggedImg service.TaskTemplate.PluginSpec.Remote = taggedImg
} }
if options.QueryRegistry { 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) resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return swarm.ServiceUpdateResponse{}, err return ServiceUpdateResult{}, err
} }
var response swarm.ServiceUpdateResponse var response swarm.ServiceUpdateResponse
err = json.NewDecoder(resp.Body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
if resolveWarning != "" { warnings = append(warnings, response.Warnings...)
response.Warnings = append(response.Warnings, resolveWarning) return ServiceUpdateResult{Warnings: warnings}, err
}
return response, err
} }

View File

@@ -17,7 +17,9 @@ type SwarmJoinOptions struct {
} }
// SwarmJoinResult contains the result of joining a swarm. // 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. // SwarmJoin joins the swarm.
func (cli *Client) SwarmJoin(ctx context.Context, options SwarmJoinOptions) (SwarmJoinResult, error) { 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" "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. // TaskInspectResult contains the result of a task inspection.
type TaskInspectResult struct { type TaskInspectResult struct {
Task swarm.Task Task swarm.Task
@@ -13,7 +18,7 @@ type TaskInspectResult struct {
} }
// TaskInspect returns the task information and its raw representation. // 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) taskID, err := trimID("task", taskID)
if err != nil { if err != nil {
return TaskInspectResult{}, err return TaskInspectResult{}, err

View File

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

View File

@@ -4,14 +4,34 @@ import (
"context" "context"
"io" "io"
"net/url" "net/url"
"sync"
"time" "time"
"github.com/moby/moby/client/internal/timestamp" "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. // 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{} query := url.Values{}
if options.ShowStdout { if options.ShowStdout {
query.Set("stdout", "1") query.Set("stdout", "1")
@@ -24,7 +44,7 @@ func (cli *Client) TaskLogs(ctx context.Context, taskID string, options Containe
if options.Since != "" { if options.Since != "" {
ts, err := timestamp.GetTimestamp(options.Since, time.Now()) ts, err := timestamp.GetTimestamp(options.Since, time.Now())
if err != nil { if err != nil {
return nil, err return TaskLogsResult{}, err
} }
query.Set("since", ts) 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) resp, err := cli.get(ctx, "/tasks/"+taskID+"/logs", query, nil)
if err != 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()
} }