From cd08b79c0245e57ab26aaa5277057ad9cec7581b Mon Sep 17 00:00:00 2001 From: Austin Vazquez Date: Tue, 21 Oct 2025 14:21:54 -0500 Subject: [PATCH] client: refactor service api client functions for defined options/result structs Co-authored-by: Claude Signed-off-by: Austin Vazquez --- client/client_interfaces.go | 16 ++--- client/service_create.go | 50 ++++++++++++---- client/service_inspect.go | 36 +++++------ client/service_inspect_test.go | 12 ++-- client/service_list.go | 20 ++++++- client/service_list_test.go | 4 +- client/service_logs.go | 58 ++++++++++++++++-- client/service_logs_test.go | 20 +++---- client/service_remove.go | 16 ++++- client/service_remove_test.go | 10 ++-- client/service_update.go | 59 +++++++++++++++---- client/swarm_join.go | 4 +- client/swarm_service_create_opts.go | 16 ----- client/swarm_service_inspect_opts.go | 7 --- client/swarm_service_list_opts.go | 10 ---- client/swarm_service_update_opts.go | 31 ---------- client/task_inspect.go | 7 ++- client/task_inspect_test.go | 8 +-- client/task_list.go | 4 +- client/task_list_test.go | 2 +- client/task_logs.go | 56 ++++++++++++++++-- integration-cli/daemon/daemon_swarm.go | 8 +-- .../docker_api_swarm_service_test.go | 8 +-- integration/internal/swarm/service.go | 4 +- integration/internal/swarm/states.go | 24 ++++---- integration/network/inspect_test.go | 3 +- integration/network/overlay/overlay_test.go | 2 +- integration/network/service_test.go | 22 +++---- integration/service/create_test.go | 44 +++++++------- integration/service/inspect_test.go | 4 +- integration/service/jobs_test.go | 18 +++--- integration/service/list_test.go | 18 +++--- integration/service/network_linux_test.go | 6 +- integration/service/update_test.go | 32 +++++----- internal/testutil/daemon/service.go | 16 ++--- .../moby/moby/client/client_interfaces.go | 16 ++--- .../moby/moby/client/service_create.go | 50 ++++++++++++---- .../moby/moby/client/service_inspect.go | 36 +++++------ .../moby/moby/client/service_list.go | 20 ++++++- .../moby/moby/client/service_logs.go | 58 ++++++++++++++++-- .../moby/moby/client/service_remove.go | 16 ++++- .../moby/moby/client/service_update.go | 59 +++++++++++++++---- .../github.com/moby/moby/client/swarm_join.go | 4 +- .../moby/client/swarm_service_create_opts.go | 16 ----- .../moby/client/swarm_service_inspect_opts.go | 7 --- .../moby/client/swarm_service_list_opts.go | 10 ---- .../moby/client/swarm_service_update_opts.go | 31 ---------- .../moby/moby/client/task_inspect.go | 7 ++- .../github.com/moby/moby/client/task_list.go | 4 +- .../github.com/moby/moby/client/task_logs.go | 56 ++++++++++++++++-- 50 files changed, 646 insertions(+), 399 deletions(-) delete mode 100644 client/swarm_service_create_opts.go delete mode 100644 client/swarm_service_inspect_opts.go delete mode 100644 client/swarm_service_list_opts.go delete mode 100644 client/swarm_service_update_opts.go delete mode 100644 vendor/github.com/moby/moby/client/swarm_service_create_opts.go delete mode 100644 vendor/github.com/moby/moby/client/swarm_service_inspect_opts.go delete mode 100644 vendor/github.com/moby/moby/client/swarm_service_list_opts.go delete mode 100644 vendor/github.com/moby/moby/client/swarm_service_update_opts.go diff --git a/client/client_interfaces.go b/client/client_interfaces.go index a19f0cdf52..7e860b599b 100644 --- a/client/client_interfaces.go +++ b/client/client_interfaces.go @@ -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) } diff --git a/client/service_create.go b/client/service_create.go index 0f56fb0a71..a9f62271a8 100644 --- a/client/service_create.go +++ b/client/service_create.go @@ -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 { diff --git a/client/service_inspect.go b/client/service_inspect.go index ab79f91d34..fabae9fb08 100644 --- a/client/service_inspect.go +++ b/client/service_inspect.go @@ -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 } diff --git a/client/service_inspect_test.go b/client/service_inspect_test.go index 195e008df2..9c600da640 100644 --- a/client/service_inspect_test.go +++ b/client/service_inspect_test.go @@ -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")) } diff --git a/client/service_list.go b/client/service_list.go index d4b77b4256..c0fb498588 100644 --- a/client/service_list.go +++ b/client/service_list.go @@ -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 } diff --git a/client/service_list_test.go b/client/service_list_test.go index f8d7a663b1..87303ec012 100644 --- a/client/service_list_test.go +++ b/client/service_list_test.go @@ -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)) } } diff --git a/client/service_logs.go b/client/service_logs.go index 352bd8f68b..cbbb958392 100644 --- a/client/service_logs.go +++ b/client/service_logs.go @@ -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() } diff --git a/client/service_logs_test.go b/client/service_logs_test.go index 1a1ce45fba..8135610bec 100644 --- a/client/service_logs_test.go +++ b/client/service_logs_test.go @@ -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) } diff --git a/client/service_remove.go b/client/service_remove.go index 0c7cc571e0..163689b693 100644 --- a/client/service_remove.go +++ b/client/service_remove.go @@ -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 } diff --git a/client/service_remove_test.go b/client/service_remove_test.go index 6be56e53ea..b6f6fd1304 100644 --- a/client/service_remove_test.go +++ b/client/service_remove_test.go @@ -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) } diff --git a/client/service_update.go b/client/service_update.go index 42e5fc9711..3910d2bc5f 100644 --- a/client/service_update.go +++ b/client/service_update.go @@ -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 } diff --git a/client/swarm_join.go b/client/swarm_join.go index ee91864409..66a7544821 100644 --- a/client/swarm_join.go +++ b/client/swarm_join.go @@ -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) { diff --git a/client/swarm_service_create_opts.go b/client/swarm_service_create_opts.go deleted file mode 100644 index 504502ecf7..0000000000 --- a/client/swarm_service_create_opts.go +++ /dev/null @@ -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 -} diff --git a/client/swarm_service_inspect_opts.go b/client/swarm_service_inspect_opts.go deleted file mode 100644 index 691f3634e4..0000000000 --- a/client/swarm_service_inspect_opts.go +++ /dev/null @@ -1,7 +0,0 @@ -package client - -// ServiceInspectOptions holds parameters related to the "service inspect" -// operation. -type ServiceInspectOptions struct { - InsertDefaults bool -} diff --git a/client/swarm_service_list_opts.go b/client/swarm_service_list_opts.go deleted file mode 100644 index 8a06f1bd3c..0000000000 --- a/client/swarm_service_list_opts.go +++ /dev/null @@ -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 -} diff --git a/client/swarm_service_update_opts.go b/client/swarm_service_update_opts.go deleted file mode 100644 index cf0cc41239..0000000000 --- a/client/swarm_service_update_opts.go +++ /dev/null @@ -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 -} diff --git a/client/task_inspect.go b/client/task_inspect.go index 6012fe8a48..277b00ff49 100644 --- a/client/task_inspect.go +++ b/client/task_inspect.go @@ -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 diff --git a/client/task_inspect_test.go b/client/task_inspect_test.go index 6a3a43742e..833266a289 100644 --- a/client/task_inspect_test.go +++ b/client/task_inspect_test.go @@ -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")) } diff --git a/client/task_list.go b/client/task_list.go index 0511c1c735..5f7c41bb9d 100644 --- a/client/task_list.go +++ b/client/task_list.go @@ -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 } diff --git a/client/task_list_test.go b/client/task_list_test.go index 0c7ec27d97..69bf8af8b6 100644 --- a/client/task_list_test.go +++ b/client/task_list_test.go @@ -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)) } } diff --git a/client/task_logs.go b/client/task_logs.go index 6ef35521ec..c42c1028a3 100644 --- a/client/task_logs.go +++ b/client/task_logs.go @@ -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() } diff --git a/integration-cli/daemon/daemon_swarm.go b/integration-cli/daemon/daemon_swarm.go index ab363b59f4..abf00f19e9 100644 --- a/integration-cli/daemon/daemon_swarm.go +++ b/integration-cli/daemon/daemon_swarm.go @@ -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]++ } diff --git a/integration-cli/docker_api_swarm_service_test.go b/integration-cli/docker_api_swarm_service_test.go index ef531b6b62..33240f7095 100644 --- a/integration-cli/docker_api_swarm_service_test.go +++ b/integration-cli/docker_api_swarm_service_test.go @@ -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")) diff --git a/integration/internal/swarm/service.go b/integration/internal/swarm/service.go index b2c3a51139..b544d2f88c 100644 --- a/integration/internal/swarm/service.go +++ b/integration/internal/swarm/service.go @@ -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 diff --git a/integration/internal/swarm/states.go b/integration/internal/swarm/states.go index f5af4d23cd..ce8ebfb928 100644 --- a/integration/internal/swarm/states.go +++ b/integration/internal/swarm/states.go @@ -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 diff --git a/integration/network/inspect_test.go b/integration/network/inspect_test.go index 5163864af4..1b880c89ef 100644 --- a/integration/network/inspect_test.go +++ b/integration/network/inspect_test.go @@ -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) }() diff --git a/integration/network/overlay/overlay_test.go b/integration/network/overlay/overlay_test.go index 9151bcb149..b2b6bdb2d1 100644 --- a/integration/network/overlay/overlay_test.go +++ b/integration/network/overlay/overlay_test.go @@ -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) diff --git a/integration/network/service_test.go b/integration/network/service_test.go index 66ea6aeb63..d2cf76de02 100644 --- a/integration/network/service_test.go +++ b/integration/network/service_test.go @@ -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) diff --git a/integration/service/create_test.go b/integration/service/create_test.go index ce967bd2d2..e2dc8d6bf8 100644 --- a/integration/service/create_test.go +++ b/integration/service/create_test.go @@ -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) } diff --git a/integration/service/inspect_test.go b/integration/service/inspect_test.go index 0f773bdfec..f186067366 100644 --- a/integration/service/inspect_test.go +++ b/integration/service/inspect_test.go @@ -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 diff --git a/integration/service/jobs_test.go b/integration/service/jobs_test.go index aa7edde12c..22b7e1559b 100644 --- a/integration/service/jobs_test.go +++ b/integration/service/jobs_test.go @@ -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) } diff --git a/integration/service/list_test.go b/integration/service/list_test.go index d4c63bc49b..4d47055ee3 100644 --- a/integration/service/list_test.go +++ b/integration/service/list_test.go @@ -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) diff --git a/integration/service/network_linux_test.go b/integration/service/network_linux_test.go index 77555ddeee..e02d365877 100644 --- a/integration/service/network_linux_test.go +++ b/integration/service/network_linux_test.go @@ -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) }() diff --git a/integration/service/update_test.go b/integration/service/update_test.go index eef3593d6b..1fa2332260 100644 --- a/integration/service/update_test.go +++ b/integration/service/update_test.go @@ -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) diff --git a/internal/testutil/daemon/service.go b/internal/testutil/daemon/service.go index 8a575b14f3..3c394c897a 100644 --- a/internal/testutil/daemon/service.go +++ b/internal/testutil/daemon/service.go @@ -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 } diff --git a/vendor/github.com/moby/moby/client/client_interfaces.go b/vendor/github.com/moby/moby/client/client_interfaces.go index a19f0cdf52..7e860b599b 100644 --- a/vendor/github.com/moby/moby/client/client_interfaces.go +++ b/vendor/github.com/moby/moby/client/client_interfaces.go @@ -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) } diff --git a/vendor/github.com/moby/moby/client/service_create.go b/vendor/github.com/moby/moby/client/service_create.go index 0f56fb0a71..a9f62271a8 100644 --- a/vendor/github.com/moby/moby/client/service_create.go +++ b/vendor/github.com/moby/moby/client/service_create.go @@ -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 { diff --git a/vendor/github.com/moby/moby/client/service_inspect.go b/vendor/github.com/moby/moby/client/service_inspect.go index ab79f91d34..fabae9fb08 100644 --- a/vendor/github.com/moby/moby/client/service_inspect.go +++ b/vendor/github.com/moby/moby/client/service_inspect.go @@ -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 } diff --git a/vendor/github.com/moby/moby/client/service_list.go b/vendor/github.com/moby/moby/client/service_list.go index d4b77b4256..c0fb498588 100644 --- a/vendor/github.com/moby/moby/client/service_list.go +++ b/vendor/github.com/moby/moby/client/service_list.go @@ -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 } diff --git a/vendor/github.com/moby/moby/client/service_logs.go b/vendor/github.com/moby/moby/client/service_logs.go index 352bd8f68b..cbbb958392 100644 --- a/vendor/github.com/moby/moby/client/service_logs.go +++ b/vendor/github.com/moby/moby/client/service_logs.go @@ -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() } diff --git a/vendor/github.com/moby/moby/client/service_remove.go b/vendor/github.com/moby/moby/client/service_remove.go index 0c7cc571e0..163689b693 100644 --- a/vendor/github.com/moby/moby/client/service_remove.go +++ b/vendor/github.com/moby/moby/client/service_remove.go @@ -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 } diff --git a/vendor/github.com/moby/moby/client/service_update.go b/vendor/github.com/moby/moby/client/service_update.go index 42e5fc9711..3910d2bc5f 100644 --- a/vendor/github.com/moby/moby/client/service_update.go +++ b/vendor/github.com/moby/moby/client/service_update.go @@ -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 } diff --git a/vendor/github.com/moby/moby/client/swarm_join.go b/vendor/github.com/moby/moby/client/swarm_join.go index ee91864409..66a7544821 100644 --- a/vendor/github.com/moby/moby/client/swarm_join.go +++ b/vendor/github.com/moby/moby/client/swarm_join.go @@ -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) { diff --git a/vendor/github.com/moby/moby/client/swarm_service_create_opts.go b/vendor/github.com/moby/moby/client/swarm_service_create_opts.go deleted file mode 100644 index 504502ecf7..0000000000 --- a/vendor/github.com/moby/moby/client/swarm_service_create_opts.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/moby/moby/client/swarm_service_inspect_opts.go b/vendor/github.com/moby/moby/client/swarm_service_inspect_opts.go deleted file mode 100644 index 691f3634e4..0000000000 --- a/vendor/github.com/moby/moby/client/swarm_service_inspect_opts.go +++ /dev/null @@ -1,7 +0,0 @@ -package client - -// ServiceInspectOptions holds parameters related to the "service inspect" -// operation. -type ServiceInspectOptions struct { - InsertDefaults bool -} diff --git a/vendor/github.com/moby/moby/client/swarm_service_list_opts.go b/vendor/github.com/moby/moby/client/swarm_service_list_opts.go deleted file mode 100644 index 8a06f1bd3c..0000000000 --- a/vendor/github.com/moby/moby/client/swarm_service_list_opts.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/moby/moby/client/swarm_service_update_opts.go b/vendor/github.com/moby/moby/client/swarm_service_update_opts.go deleted file mode 100644 index cf0cc41239..0000000000 --- a/vendor/github.com/moby/moby/client/swarm_service_update_opts.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/moby/moby/client/task_inspect.go b/vendor/github.com/moby/moby/client/task_inspect.go index 6012fe8a48..277b00ff49 100644 --- a/vendor/github.com/moby/moby/client/task_inspect.go +++ b/vendor/github.com/moby/moby/client/task_inspect.go @@ -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 diff --git a/vendor/github.com/moby/moby/client/task_list.go b/vendor/github.com/moby/moby/client/task_list.go index 0511c1c735..5f7c41bb9d 100644 --- a/vendor/github.com/moby/moby/client/task_list.go +++ b/vendor/github.com/moby/moby/client/task_list.go @@ -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 } diff --git a/vendor/github.com/moby/moby/client/task_logs.go b/vendor/github.com/moby/moby/client/task_logs.go index 6ef35521ec..c42c1028a3 100644 --- a/vendor/github.com/moby/moby/client/task_logs.go +++ b/vendor/github.com/moby/moby/client/task_logs.go @@ -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() }