Merge pull request #51252 from austinvazquez/refactor-client-service

client: refactor service api client functions for defined options/res…
This commit is contained in:
Austin Vazquez
2025-10-21 20:25:13 -05:00
committed by GitHub
50 changed files with 646 additions and 399 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,11 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// TaskInspectOptions contains options for inspecting a task.
type TaskInspectOptions struct {
// Currently no options are defined.
}
// TaskInspectResult contains the result of a task inspection.
type TaskInspectResult struct {
Task swarm.Task
@@ -13,7 +18,7 @@ type TaskInspectResult struct {
}
// TaskInspect returns the task information and its raw representation.
func (cli *Client) TaskInspect(ctx context.Context, taskID string) (TaskInspectResult, error) {
func (cli *Client) TaskInspect(ctx context.Context, taskID string, options TaskInspectOptions) (TaskInspectResult, error) {
taskID, err := trimID("task", taskID)
if err != nil {
return TaskInspectResult{}, err

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,14 +78,14 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesCreate(c *testing.T) {
options := client.ServiceInspectOptions{InsertDefaults: true}
// insertDefaults inserts UpdateConfig when service is fetched by ID
resp, _, err := apiClient.ServiceInspectWithRaw(ctx, id, options)
out := fmt.Sprintf("%+v", resp)
res, err := apiClient.ServiceInspect(ctx, id, options)
out := fmt.Sprintf("%+v", res.Service)
assert.NilError(c, err)
assert.Assert(c, is.Contains(out, "UpdateConfig"))
// insertDefaults inserts UpdateConfig when service is fetched by ID
resp, _, err = apiClient.ServiceInspectWithRaw(ctx, "top", options)
out = fmt.Sprintf("%+v", resp)
res, err = apiClient.ServiceInspect(ctx, "top", options)
out = fmt.Sprintf("%+v", res.Service)
assert.NilError(c, err)
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 {
t.Helper()
result, err := c.TaskList(ctx, client.TaskListOptions{
taskList, err := c.TaskList(ctx, client.TaskListOptions{
Filters: make(client.Filters).
Add("service", serviceID).
Add("desired-state", "running"),
})
assert.NilError(t, err)
return result.Tasks
return taskList.Items
}
// ExecTask runs the passed in exec config on the given task

View File

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

View File

@@ -76,7 +76,8 @@ func TestInspectNetwork(t *testing.T) {
swarm.ServiceWithNetwork(networkName),
)
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)
}()

View File

@@ -85,7 +85,7 @@ func TestHostPortMappings(t *testing.T) {
{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)

View File

@@ -245,10 +245,10 @@ func TestServiceWithPredefinedNetwork(t *testing.T) {
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)
err = c.ServiceRemove(ctx, serviceID)
_, err = c.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err)
}
@@ -286,10 +286,10 @@ func TestServiceRemoveKeepsIngressNetwork(t *testing.T) {
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)
err = c.ServiceRemove(ctx, serviceID)
_, err = c.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err)
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 {
return func(log poll.LogT) poll.Result {
services, err := apiClient.ServiceList(ctx, client.ServiceListOptions{})
result, err := apiClient.ServiceList(ctx, client.ServiceListOptions{})
switch {
case err != nil:
return poll.Error(err)
case len(services) == 0:
case len(result.Services) == 0:
return poll.Success()
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)
assert.Equal(t, info.Swarm.Cluster.DataPathPort, datapathPort)
err := c.ServiceRemove(ctx, serviceID)
_, err := c.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err)
poll.WaitOn(t, noServices(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)
var defaultDataPathPort uint32 = 4789
assert.Equal(t, info.Swarm.Cluster.DataPathPort, defaultDataPathPort)
err = nc.ServiceRemove(ctx, serviceID)
_, err = nc.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err)
poll.WaitOn(t, noServices(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)
_, _, err := cli.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{})
_, err := cli.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err)
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.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, swarm.NoTasks(ctx, cli), swarm.ServicePoll)
assert.NilError(t, err)

View File

@@ -100,10 +100,10 @@ func TestCreateServiceMultipleTimes(t *testing.T) {
serviceID := swarm.CreateService(ctx, t, d, serviceSpec...)
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)
err = apiClient.ServiceRemove(ctx, serviceID)
_, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err)
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...)
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)
// 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...)
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)
}
@@ -227,7 +227,7 @@ func TestCreateServiceSecretFileMode(t *testing.T) {
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",
ShowStdout: true,
})
@@ -238,7 +238,7 @@ func TestCreateServiceSecretFileMode(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.Contains(string(content), "-rwxrwxrwx"))
err = apiClient.ServiceRemove(ctx, serviceID)
_, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err)
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))
body, err := apiClient.ServiceLogs(ctx, serviceID, client.ContainerLogsOptions{
body, err := apiClient.ServiceLogs(ctx, serviceID, client.ServiceLogsOptions{
Tail: "1",
ShowStdout: true,
})
@@ -297,7 +297,7 @@ func TestCreateServiceConfigFileMode(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.Contains(string(content), "-rwxrwxrwx"))
err = apiClient.ServiceRemove(ctx, serviceID)
_, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
assert.NilError(t, err)
poll.WaitOn(t, swarm.NoTasksForService(ctx, apiClient, serviceID))
@@ -368,25 +368,25 @@ func TestCreateServiceSysctls(t *testing.T) {
// more complex)
// 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),
})
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
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.DeepEqual(t, ctnr.HostConfig.Sysctls, expectedSysctls)
// 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.
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{})
result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err)
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.
// 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),
})
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
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.DeepEqual(t, ctnr.HostConfig.CapAdd, capAdd)
assert.DeepEqual(t, ctnr.HostConfig.CapDrop, capDrop)
// verify that the task has the capabilities option set in the task object
assert.DeepEqual(t, taskResult.Tasks[0].Spec.ContainerSpec.CapabilityAdd, capAdd)
assert.DeepEqual(t, taskResult.Tasks[0].Spec.ContainerSpec.CapabilityDrop, capDrop)
assert.DeepEqual(t, taskList.Items[0].Spec.ContainerSpec.CapabilityAdd, capAdd)
assert.DeepEqual(t, taskList.Items[0].Spec.ContainerSpec.CapabilityDrop, capDrop)
// verify that the service also has the capabilities set in the spec.
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{})
result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
assert.NilError(t, err)
assert.DeepEqual(t, service.Spec.TaskTemplate.ContainerSpec.CapabilityAdd, capAdd)
assert.DeepEqual(t, service.Spec.TaskTemplate.ContainerSpec.CapabilityDrop, capDrop)
assert.DeepEqual(t, result.Service.Spec.TaskTemplate.ContainerSpec.CapabilityAdd, capAdd)
assert.DeepEqual(t, result.Service.Spec.TaskTemplate.ContainerSpec.CapabilityDrop, capDrop)
}

View File

@@ -38,7 +38,7 @@ func TestInspect(t *testing.T) {
id := resp.ID
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)
expected := swarmtypes.Service{
@@ -50,7 +50,7 @@ func TestInspect(t *testing.T) {
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

View File

@@ -74,12 +74,12 @@ func TestReplicatedJob(t *testing.T) {
swarm.ServiceWithCommand([]string{"true"}),
)
service, _, err := apiClient.ServiceInspectWithRaw(
result, err := apiClient.ServiceInspect(
ctx, id, client.ServiceInspectOptions{},
)
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
@@ -107,33 +107,33 @@ func TestUpdateReplicatedJob(t *testing.T) {
swarm.ServiceWithCommand([]string{"true"}),
)
service, _, err := apiClient.ServiceInspectWithRaw(
result, err := apiClient.ServiceInspect(
ctx, id, client.ServiceInspectOptions{},
)
assert.NilError(t, err)
// 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.
spec := service.Spec
spec := result.Service.Spec
spec.TaskTemplate.ForceUpdate++
_, err = apiClient.ServiceUpdate(
ctx, id, service.Version, spec, client.ServiceUpdateOptions{},
ctx, id, result.Service.Version, spec, client.ServiceUpdateOptions{},
)
assert.NilError(t, err)
service2, _, err := apiClient.ServiceInspectWithRaw(
result2, err := apiClient.ServiceInspect(
ctx, id, client.ServiceInspectOptions{},
)
assert.NilError(t, err)
// assert that the job iteration has increased
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.
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
// bespoke closure right here.
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),
})
running := 0
for _, task := range taskResult.Tasks {
for _, task := range taskList.Items {
if task.Status.State == swarmtypes.TaskStateRunning {
running++
}
@@ -71,25 +71,25 @@ func TestServiceListWithStatuses(t *testing.T) {
default:
return poll.Continue(
"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.
resp, err := apiClient.ServiceList(ctx, client.ServiceListOptions{})
result, err := apiClient.ServiceList(ctx, client.ServiceListOptions{})
assert.NilError(t, err)
assert.Check(t, is.Len(resp, serviceCount))
for _, service := range resp {
assert.Check(t, is.Len(result.Services, serviceCount))
for _, service := range result.Services {
assert.Check(t, is.Nil(service.ServiceStatus))
}
// 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.Check(t, is.Len(resp, serviceCount))
for _, service := range resp {
assert.Check(t, is.Len(result.Services, serviceCount))
for _, service := range result.Services {
replicas := *service.Spec.Mode.Replicated.Replicas
assert.Assert(t, service.ServiceStatus != nil)

View File

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

View File

@@ -68,7 +68,7 @@ func TestServiceUpdateLabel(t *testing.T) {
service = getService(ctx, t, apiClient, serviceID)
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)
}
@@ -130,7 +130,7 @@ func TestServiceUpdateSecrets(t *testing.T) {
service = getService(ctx, t, apiClient, serviceID)
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)
}
@@ -194,7 +194,7 @@ func TestServiceUpdateConfigs(t *testing.T) {
service = getService(ctx, t, apiClient, serviceID)
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)
}
@@ -246,7 +246,7 @@ func TestServiceUpdateNetwork(t *testing.T) {
err = apiClient.NetworkRemove(ctx, overlayID)
assert.NilError(t, err)
err = apiClient.ServiceRemove(ctx, serviceID)
_, err = apiClient.ServiceRemove(ctx, serviceID, client.ServiceRemoveOptions{})
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)
}
func getServiceTaskContainer(ctx context.Context, t *testing.T, cli client.APIClient, serviceID string) container.InspectResponse {
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"),
})
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.Equal(t, ctr.State.Running, true)
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 {
t.Helper()
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{})
result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
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 {
return func(log poll.LogT) poll.Result {
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{})
switch {
case err != nil:
result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
if err != nil {
return poll.Error(err)
}
service := result.Service
switch {
case service.UpdateStatus != nil && service.UpdateStatus.State == swarmtypes.UpdateStateCompleted:
return poll.Success()
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 {
return func(log poll.LogT) poll.Result {
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, client.ServiceInspectOptions{})
result, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{})
switch {
case err != nil:
return poll.Error(err)
case service.Version.Index > serviceOldVersion:
case result.Service.Version.Index > serviceOldVersion:
return poll.Success()
default:
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)
defer cli.Close()
service, _, err := cli.ServiceInspectWithRaw(ctx, id, client.ServiceInspectOptions{})
res, err := cli.ServiceInspect(ctx, id, client.ServiceInspectOptions{})
assert.NilError(t, err)
return &service
return &res.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,
}
result, err := cli.TaskList(ctx, options)
taskList, err := cli.TaskList(ctx, options)
assert.NilError(t, err)
return result.Tasks
return taskList.Items
}
// 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)
defer cli.Close()
err := cli.ServiceRemove(ctx, id)
_, err := cli.ServiceRemove(ctx, id, client.ServiceRemoveOptions{})
assert.NilError(t, err)
}
@@ -105,9 +105,9 @@ func (d *Daemon) ListServices(ctx context.Context, t testing.TB) []swarm.Service
cli := d.NewClientT(t)
defer cli.Close()
services, err := cli.ServiceList(ctx, client.ServiceListOptions{})
res, err := cli.ServiceList(ctx, client.ServiceListOptions{})
assert.NilError(t, err)
return services
return res.Services
}
// 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)
defer cli.Close()
result, err := cli.TaskInspect(ctx, id)
result, err := cli.TaskInspect(ctx, id, client.TaskInspectOptions{})
assert.NilError(t, err)
return result.Task
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,11 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// TaskInspectOptions contains options for inspecting a task.
type TaskInspectOptions struct {
// Currently no options are defined.
}
// TaskInspectResult contains the result of a task inspection.
type TaskInspectResult struct {
Task swarm.Task
@@ -13,7 +18,7 @@ type TaskInspectResult struct {
}
// TaskInspect returns the task information and its raw representation.
func (cli *Client) TaskInspect(ctx context.Context, taskID string) (TaskInspectResult, error) {
func (cli *Client) TaskInspect(ctx context.Context, taskID string, options TaskInspectOptions) (TaskInspectResult, error) {
taskID, err := trimID("task", taskID)
if err != nil {
return TaskInspectResult{}, err

View File

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

View File

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