mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
client: refactor service api client functions for defined options/result structs
Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package client
|
||||
|
||||
// ServiceInspectOptions holds parameters related to the "service inspect"
|
||||
// operation.
|
||||
type ServiceInspectOptions struct {
|
||||
InsertDefaults bool
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user