client/node: Wrap options and output

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski
2025-10-22 12:01:37 +02:00
parent 25c509b026
commit 7ceea4148a
22 changed files with 145 additions and 76 deletions

View File

@@ -137,10 +137,10 @@ type NetworkAPIClient interface {
// NodeAPIClient defines API client methods for the nodes
type NodeAPIClient interface {
NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error)
NodeList(ctx context.Context, options NodeListOptions) ([]swarm.Node, error)
NodeRemove(ctx context.Context, nodeID string, options NodeRemoveOptions) error
NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error
NodeInspect(ctx context.Context, nodeID string, options NodeInspectOptions) (NodeInspectResult, error)
NodeList(ctx context.Context, options NodeListOptions) (NodeListResult, error)
NodeRemove(ctx context.Context, nodeID string, options NodeRemoveOptions) (NodeRemoveResult, error)
NodeUpdate(ctx context.Context, nodeID string, options NodeUpdateOptions) (NodeUpdateResult, error)
}
// PluginAPIClient defines API client methods for the plugins

View File

@@ -9,25 +9,30 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// NodeInspectWithRaw returns the node information.
func (cli *Client) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) {
type NodeInspectResult struct {
Node swarm.Node
Raw []byte
}
// NodeInspect returns the node information.
func (cli *Client) NodeInspect(ctx context.Context, nodeID string, options NodeInspectOptions) (NodeInspectResult, error) {
nodeID, err := trimID("node", nodeID)
if err != nil {
return swarm.Node{}, nil, err
return NodeInspectResult{}, err
}
resp, err := cli.get(ctx, "/nodes/"+nodeID, nil, nil)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.Node{}, nil, err
return NodeInspectResult{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return swarm.Node{}, nil, err
return NodeInspectResult{}, err
}
var response swarm.Node
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
return NodeInspectResult{Node: response, Raw: body}, err
}

View File

@@ -19,7 +19,7 @@ func TestNodeInspectError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
_, _, err = client.NodeInspectWithRaw(context.Background(), "nothing")
_, err = client.NodeInspect(context.Background(), "nothing", NodeInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
}
@@ -27,7 +27,7 @@ func TestNodeInspectNodeNotFound(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusNotFound, "Server error")))
assert.NilError(t, err)
_, _, err = client.NodeInspectWithRaw(context.Background(), "unknown")
_, err = client.NodeInspect(context.Background(), "unknown", NodeInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
}
@@ -36,11 +36,11 @@ func TestNodeInspectWithEmptyID(t *testing.T) {
return nil, errors.New("should not make request")
}))
assert.NilError(t, err)
_, _, err = client.NodeInspectWithRaw(context.Background(), "")
_, err = client.NodeInspect(context.Background(), "", NodeInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, _, err = client.NodeInspectWithRaw(context.Background(), " ")
_, err = client.NodeInspect(context.Background(), " ", NodeInspectOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -64,7 +64,7 @@ func TestNodeInspect(t *testing.T) {
}))
assert.NilError(t, err)
nodeInspect, _, err := client.NodeInspectWithRaw(context.Background(), "node_id")
result, err := client.NodeInspect(context.Background(), "node_id", NodeInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(nodeInspect.ID, "node_id"))
assert.Check(t, is.Equal(result.Node.ID, "node_id"))
}

View File

@@ -8,17 +8,21 @@ import (
"github.com/moby/moby/api/types/swarm"
)
type NodeListResult struct {
Items []swarm.Node
}
// NodeList returns the list of nodes.
func (cli *Client) NodeList(ctx context.Context, options NodeListOptions) ([]swarm.Node, error) {
func (cli *Client) NodeList(ctx context.Context, options NodeListOptions) (NodeListResult, error) {
query := url.Values{}
options.Filters.updateURLValues(query)
resp, err := cli.get(ctx, "/nodes", query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
return NodeListResult{}, err
}
var nodes []swarm.Node
err = json.NewDecoder(resp.Body).Decode(&nodes)
return nodes, err
return NodeListResult{Items: nodes}, err
}

View File

@@ -76,8 +76,8 @@ func TestNodeList(t *testing.T) {
}))
assert.NilError(t, err)
nodes, err := client.NodeList(context.Background(), listCase.options)
result, err := client.NodeList(context.Background(), listCase.options)
assert.NilError(t, err)
assert.Check(t, is.Len(nodes, 2))
assert.Check(t, is.Len(result.Items, 2))
}
}

View File

@@ -5,11 +5,14 @@ import (
"net/url"
)
type NodeRemoveResult struct {
}
// NodeRemove removes a Node.
func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options NodeRemoveOptions) error {
func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options NodeRemoveOptions) (NodeRemoveResult, error) {
nodeID, err := trimID("node", nodeID)
if err != nil {
return err
return NodeRemoveResult{}, err
}
query := url.Values{}
@@ -19,5 +22,5 @@ func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options NodeRe
resp, err := cli.delete(ctx, "/nodes/"+nodeID, query, nil)
defer ensureReaderClosed(resp)
return err
return NodeRemoveResult{}, err
}

View File

@@ -17,14 +17,14 @@ func TestNodeRemoveError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.NodeRemove(context.Background(), "node_id", NodeRemoveOptions{Force: false})
_, err = client.NodeRemove(context.Background(), "node_id", NodeRemoveOptions{Force: false})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.NodeRemove(context.Background(), "", NodeRemoveOptions{Force: false})
_, err = client.NodeRemove(context.Background(), "", NodeRemoveOptions{Force: false})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.NodeRemove(context.Background(), " ", NodeRemoveOptions{Force: false})
_, err = client.NodeRemove(context.Background(), " ", NodeRemoveOptions{Force: false})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -62,7 +62,7 @@ func TestNodeRemove(t *testing.T) {
}))
assert.NilError(t, err)
err = client.NodeRemove(context.Background(), "node_id", NodeRemoveOptions{Force: removeCase.force})
_, err = client.NodeRemove(context.Background(), "node_id", NodeRemoveOptions{Force: removeCase.force})
assert.NilError(t, err)
}
}

View File

@@ -3,20 +3,21 @@ package client
import (
"context"
"net/url"
"github.com/moby/moby/api/types/swarm"
)
type NodeUpdateResult struct {
}
// NodeUpdate updates a Node.
func (cli *Client) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error {
func (cli *Client) NodeUpdate(ctx context.Context, nodeID string, options NodeUpdateOptions) (NodeUpdateResult, error) {
nodeID, err := trimID("node", nodeID)
if err != nil {
return err
return NodeUpdateResult{}, err
}
query := url.Values{}
query.Set("version", version.String())
resp, err := cli.post(ctx, "/nodes/"+nodeID+"/update", query, node, nil)
query.Set("version", options.Version.String())
resp, err := cli.post(ctx, "/nodes/"+nodeID+"/update", query, options.Node, nil)
defer ensureReaderClosed(resp)
return err
return NodeUpdateResult{}, err
}

View File

@@ -17,14 +17,23 @@ func TestNodeUpdateError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err)
err = client.NodeUpdate(context.Background(), "node_id", swarm.Version{}, swarm.NodeSpec{})
_, err = client.NodeUpdate(context.Background(), "node_id", NodeUpdateOptions{
Version: swarm.Version{},
Node: swarm.NodeSpec{},
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
err = client.NodeUpdate(context.Background(), "", swarm.Version{}, swarm.NodeSpec{})
_, err = client.NodeUpdate(context.Background(), "", NodeUpdateOptions{
Version: swarm.Version{},
Node: swarm.NodeSpec{},
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
err = client.NodeUpdate(context.Background(), " ", swarm.Version{}, swarm.NodeSpec{})
_, err = client.NodeUpdate(context.Background(), " ", NodeUpdateOptions{
Version: swarm.Version{},
Node: swarm.NodeSpec{},
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -43,6 +52,9 @@ func TestNodeUpdate(t *testing.T) {
}))
assert.NilError(t, err)
err = client.NodeUpdate(context.Background(), "node_id", swarm.Version{}, swarm.NodeSpec{})
_, err = client.NodeUpdate(context.Background(), "node_id", NodeUpdateOptions{
Version: swarm.Version{},
Node: swarm.NodeSpec{},
})
assert.NilError(t, err)
}

View File

@@ -0,0 +1,5 @@
package client
// NodeInspectOptions holds parameters to inspect nodes with.
type NodeInspectOptions struct {
}

View File

@@ -0,0 +1,9 @@
package client
import "github.com/moby/moby/api/types/swarm"
// NodeUpdateOptions holds parameters to update nodes with.
type NodeUpdateOptions struct {
Version swarm.Version
Node swarm.NodeSpec
}

View File

@@ -178,12 +178,12 @@ func (d *Daemon) CheckLeader(ctx context.Context) func(t *testing.T) (any, strin
errList := "could not get node list"
ls, err := cli.NodeList(ctx, client.NodeListOptions{})
result, err := cli.NodeList(ctx, client.NodeListOptions{})
if err != nil {
return err, errList
}
for _, node := range ls {
for _, node := range result.Items {
if node.ManagerStatus != nil && node.ManagerStatus.Leader {
return nil, ""
}

View File

@@ -159,13 +159,13 @@ func JobComplete(ctx context.Context, apiClient client.ServiceAPIClient, service
func HasLeader(ctx context.Context, apiClient client.NodeAPIClient) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result {
nodes, err := apiClient.NodeList(ctx, client.NodeListOptions{
result, err := apiClient.NodeList(ctx, client.NodeListOptions{
Filters: make(client.Filters).Add("role", "manager"),
})
if err != nil {
return poll.Error(err)
}
for _, node := range nodes {
for _, node := range result.Items {
if node.ManagerStatus != nil && node.ManagerStatus.Leader {
return poll.Success()
}

View File

@@ -165,11 +165,11 @@ func TestInspectNetwork(t *testing.T) {
t.Run("BeforeLeaderChange", checkNetworkInspect)
leaderID := func() string {
ls, err := c1.NodeList(ctx, client.NodeListOptions{
result, err := c1.NodeList(ctx, client.NodeListOptions{
Filters: make(client.Filters).Add("role", "manager"),
})
assert.NilError(t, err)
for _, node := range ls {
for _, node := range result.Items {
if node.ManagerStatus != nil && node.ManagerStatus.Leader {
return node.ID
}

View File

@@ -20,7 +20,7 @@ func (d *Daemon) GetNode(ctx context.Context, t testing.TB, id string, errCheck
cli := d.NewClientT(t)
defer cli.Close()
node, _, err := cli.NodeInspectWithRaw(ctx, id)
result, err := cli.NodeInspect(ctx, id, client.NodeInspectOptions{})
if err != nil {
for _, f := range errCheck {
if f(err) {
@@ -28,9 +28,9 @@ func (d *Daemon) GetNode(ctx context.Context, t testing.TB, id string, errCheck
}
}
}
assert.NilError(t, err, "[%s] (*Daemon).GetNode: NodeInspectWithRaw(%q) failed", d.id, id)
assert.Check(t, node.ID == id)
return &node
assert.NilError(t, err, "[%s] (*Daemon).GetNode: NodeInspect(%q) failed", d.id, id)
assert.Check(t, result.Node.ID == id)
return &result.Node
}
// RemoveNode removes the specified node
@@ -42,7 +42,7 @@ func (d *Daemon) RemoveNode(ctx context.Context, t testing.TB, id string, force
options := client.NodeRemoveOptions{
Force: force,
}
err := cli.NodeRemove(ctx, id, options)
_, err := cli.NodeRemove(ctx, id, options)
assert.NilError(t, err)
}
@@ -58,7 +58,10 @@ func (d *Daemon) UpdateNode(ctx context.Context, t testing.TB, id string, f ...N
fn(node)
}
err := cli.NodeUpdate(ctx, node.ID, node.Version, node.Spec)
_, err := cli.NodeUpdate(ctx, node.ID, client.NodeUpdateOptions{
Version: node.Version,
Node: node.Spec,
})
if i < 10 && err != nil && strings.Contains(err.Error(), "update out of sequence") {
time.Sleep(100 * time.Millisecond)
continue
@@ -74,8 +77,8 @@ func (d *Daemon) ListNodes(ctx context.Context, t testing.TB) []swarm.Node {
cli := d.NewClientT(t)
defer cli.Close()
nodes, err := cli.NodeList(ctx, client.NodeListOptions{})
result, err := cli.NodeList(ctx, client.NodeListOptions{})
assert.NilError(t, err)
return nodes
return result.Items
}

View File

@@ -137,10 +137,10 @@ type NetworkAPIClient interface {
// NodeAPIClient defines API client methods for the nodes
type NodeAPIClient interface {
NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error)
NodeList(ctx context.Context, options NodeListOptions) ([]swarm.Node, error)
NodeRemove(ctx context.Context, nodeID string, options NodeRemoveOptions) error
NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error
NodeInspect(ctx context.Context, nodeID string, options NodeInspectOptions) (NodeInspectResult, error)
NodeList(ctx context.Context, options NodeListOptions) (NodeListResult, error)
NodeRemove(ctx context.Context, nodeID string, options NodeRemoveOptions) (NodeRemoveResult, error)
NodeUpdate(ctx context.Context, nodeID string, options NodeUpdateOptions) (NodeUpdateResult, error)
}
// PluginAPIClient defines API client methods for the plugins

View File

@@ -9,25 +9,30 @@ import (
"github.com/moby/moby/api/types/swarm"
)
// NodeInspectWithRaw returns the node information.
func (cli *Client) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) {
type NodeInspectResult struct {
Node swarm.Node
Raw []byte
}
// NodeInspect returns the node information.
func (cli *Client) NodeInspect(ctx context.Context, nodeID string, options NodeInspectOptions) (NodeInspectResult, error) {
nodeID, err := trimID("node", nodeID)
if err != nil {
return swarm.Node{}, nil, err
return NodeInspectResult{}, err
}
resp, err := cli.get(ctx, "/nodes/"+nodeID, nil, nil)
defer ensureReaderClosed(resp)
if err != nil {
return swarm.Node{}, nil, err
return NodeInspectResult{}, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return swarm.Node{}, nil, err
return NodeInspectResult{}, err
}
var response swarm.Node
rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&response)
return response, body, err
return NodeInspectResult{Node: response, Raw: body}, err
}

View File

@@ -8,17 +8,21 @@ import (
"github.com/moby/moby/api/types/swarm"
)
type NodeListResult struct {
Items []swarm.Node
}
// NodeList returns the list of nodes.
func (cli *Client) NodeList(ctx context.Context, options NodeListOptions) ([]swarm.Node, error) {
func (cli *Client) NodeList(ctx context.Context, options NodeListOptions) (NodeListResult, error) {
query := url.Values{}
options.Filters.updateURLValues(query)
resp, err := cli.get(ctx, "/nodes", query, nil)
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
return NodeListResult{}, err
}
var nodes []swarm.Node
err = json.NewDecoder(resp.Body).Decode(&nodes)
return nodes, err
return NodeListResult{Items: nodes}, err
}

View File

@@ -5,11 +5,14 @@ import (
"net/url"
)
type NodeRemoveResult struct {
}
// NodeRemove removes a Node.
func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options NodeRemoveOptions) error {
func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options NodeRemoveOptions) (NodeRemoveResult, error) {
nodeID, err := trimID("node", nodeID)
if err != nil {
return err
return NodeRemoveResult{}, err
}
query := url.Values{}
@@ -19,5 +22,5 @@ func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options NodeRe
resp, err := cli.delete(ctx, "/nodes/"+nodeID, query, nil)
defer ensureReaderClosed(resp)
return err
return NodeRemoveResult{}, err
}

View File

@@ -3,20 +3,21 @@ package client
import (
"context"
"net/url"
"github.com/moby/moby/api/types/swarm"
)
type NodeUpdateResult struct {
}
// NodeUpdate updates a Node.
func (cli *Client) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error {
func (cli *Client) NodeUpdate(ctx context.Context, nodeID string, options NodeUpdateOptions) (NodeUpdateResult, error) {
nodeID, err := trimID("node", nodeID)
if err != nil {
return err
return NodeUpdateResult{}, err
}
query := url.Values{}
query.Set("version", version.String())
resp, err := cli.post(ctx, "/nodes/"+nodeID+"/update", query, node, nil)
query.Set("version", options.Version.String())
resp, err := cli.post(ctx, "/nodes/"+nodeID+"/update", query, options.Node, nil)
defer ensureReaderClosed(resp)
return err
return NodeUpdateResult{}, err
}

View File

@@ -0,0 +1,5 @@
package client
// NodeInspectOptions holds parameters to inspect nodes with.
type NodeInspectOptions struct {
}

View File

@@ -0,0 +1,9 @@
package client
import "github.com/moby/moby/api/types/swarm"
// NodeUpdateOptions holds parameters to update nodes with.
type NodeUpdateOptions struct {
Version swarm.Version
Node swarm.NodeSpec
}