Browse code

Change "service inspect" to show defaults in place of empty fields

This adds a new parameter insertDefaults to /services/{id}. When this is
set, an empty field (such as UpdateConfig) will be populated with
default values in the API response. Make "service inspect" use this, so
that empty fields do not result in missing information when inspecting a
service.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2017/03/31 09:15:54
Showing 20 changed files
... ...
@@ -17,7 +17,7 @@ type Backend interface {
17 17
 	GetUnlockKey() (string, error)
18 18
 	UnlockSwarm(req types.UnlockRequest) error
19 19
 	GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
20
-	GetService(string) (types.Service, error)
20
+	GetService(idOrName string, insertDefaults bool) (types.Service, error)
21 21
 	CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error)
22 22
 	UpdateService(string, uint64, types.ServiceSpec, basictypes.ServiceUpdateOptions) (*basictypes.ServiceUpdateResponse, error)
23 23
 	RemoveService(string) error
... ...
@@ -30,7 +30,7 @@ type Backend interface {
30 30
 	GetTask(string) (types.Task, error)
31 31
 	GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error)
32 32
 	CreateSecret(s types.SecretSpec) (string, error)
33
-	RemoveSecret(id string) error
33
+	RemoveSecret(idOrName string) error
34 34
 	GetSecret(id string) (types.Secret, error)
35
-	UpdateSecret(id string, version uint64, spec types.SecretSpec) error
35
+	UpdateSecret(idOrName string, version uint64, spec types.SecretSpec) error
36 36
 }
... ...
@@ -151,7 +151,17 @@ func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r
151 151
 }
152 152
 
153 153
 func (sr *swarmRouter) getService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
154
-	service, err := sr.backend.GetService(vars["id"])
154
+	var insertDefaults bool
155
+	if value := r.URL.Query().Get("insertDefaults"); value != "" {
156
+		var err error
157
+		insertDefaults, err = strconv.ParseBool(value)
158
+		if err != nil {
159
+			err := fmt.Errorf("invalid value for insertDefaults: %s", value)
160
+			return errors.NewBadRequestError(err)
161
+		}
162
+	}
163
+
164
+	service, err := sr.backend.GetService(vars["id"], insertDefaults)
155 165
 	if err != nil {
156 166
 		logrus.Errorf("Error getting service %s: %v", vars["id"], err)
157 167
 		return err
... ...
@@ -39,7 +39,7 @@ func (sr *swarmRouter) swarmLogs(ctx context.Context, w http.ResponseWriter, r *
39 39
 	// checking for whether logs are TTY involves iterating over every service
40 40
 	// and task. idk if there is a better way
41 41
 	for _, service := range selector.Services {
42
-		s, err := sr.backend.GetService(service)
42
+		s, err := sr.backend.GetService(service, false)
43 43
 		if err != nil {
44 44
 			// maybe should return some context with this error?
45 45
 			return err
... ...
@@ -7584,6 +7584,11 @@ paths:
7584 7584
           description: "ID or name of service."
7585 7585
           required: true
7586 7586
           type: "string"
7587
+        - name: "insertDefaults"
7588
+          in: "query"
7589
+          description: "Fill empty fields with default values."
7590
+          type: "boolean"
7591
+          default: false
7587 7592
       tags: ["Service"]
7588 7593
     delete:
7589 7594
       summary: "Delete a service"
... ...
@@ -315,12 +315,18 @@ type ServiceUpdateOptions struct {
315 315
 	Rollback string
316 316
 }
317 317
 
318
-// ServiceListOptions holds parameters to list  services with.
318
+// ServiceListOptions holds parameters to list services with.
319 319
 type ServiceListOptions struct {
320 320
 	Filters filters.Args
321 321
 }
322 322
 
323
-// TaskListOptions holds parameters to list  tasks with.
323
+// ServiceInspectOptions holds parameters related to the "service inspect"
324
+// operation.
325
+type ServiceInspectOptions struct {
326
+	InsertDefaults bool
327
+}
328
+
329
+// TaskListOptions holds parameters to list tasks with.
324 330
 type TaskListOptions struct {
325 331
 	Filters filters.Args
326 332
 }
... ...
@@ -1,6 +1,7 @@
1 1
 package idresolver
2 2
 
3 3
 import (
4
+	"github.com/docker/docker/api/types"
4 5
 	"github.com/docker/docker/api/types/swarm"
5 6
 	"github.com/docker/docker/client"
6 7
 	"golang.org/x/net/context"
... ...
@@ -19,7 +20,7 @@ func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, nodeID string) (s
19 19
 	return swarm.Node{}, []byte{}, nil
20 20
 }
21 21
 
22
-func (cli *fakeClient) ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error) {
22
+func (cli *fakeClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error) {
23 23
 	if cli.serviceInspectFunc != nil {
24 24
 		return cli.serviceInspectFunc(serviceID)
25 25
 	}
... ...
@@ -3,6 +3,7 @@ package idresolver
3 3
 import (
4 4
 	"golang.org/x/net/context"
5 5
 
6
+	"github.com/docker/docker/api/types"
6 7
 	"github.com/docker/docker/api/types/swarm"
7 8
 	"github.com/docker/docker/client"
8 9
 	"github.com/pkg/errors"
... ...
@@ -39,7 +40,7 @@ func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string,
39 39
 		}
40 40
 		return id, nil
41 41
 	case swarm.Service:
42
-		service, _, err := r.client.ServiceInspectWithRaw(ctx, id)
42
+		service, _, err := r.client.ServiceInspectWithRaw(ctx, id, types.ServiceInspectOptions{})
43 43
 		if err != nil {
44 44
 			return id, nil
45 45
 		}
... ...
@@ -5,6 +5,7 @@ import (
5 5
 
6 6
 	"golang.org/x/net/context"
7 7
 
8
+	"github.com/docker/docker/api/types"
8 9
 	"github.com/docker/docker/cli"
9 10
 	"github.com/docker/docker/cli/command"
10 11
 	"github.com/docker/docker/cli/command/formatter"
... ...
@@ -51,7 +52,8 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
51 51
 	}
52 52
 
53 53
 	getRef := func(ref string) (interface{}, []byte, error) {
54
-		service, _, err := client.ServiceInspectWithRaw(ctx, ref)
54
+		// Service inspect shows defaults values in empty fields.
55
+		service, _, err := client.ServiceInspectWithRaw(ctx, ref, types.ServiceInspectOptions{InsertDefaults: true})
55 56
 		if err == nil || !apiclient.IsErrServiceNotFound(err) {
56 57
 			return service, nil, err
57 58
 		}
... ...
@@ -84,7 +84,7 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
84 84
 		tty          bool
85 85
 	)
86 86
 
87
-	service, _, err := cli.ServiceInspectWithRaw(ctx, opts.target)
87
+	service, _, err := cli.ServiceInspectWithRaw(ctx, opts.target, types.ServiceInspectOptions{})
88 88
 	if err != nil {
89 89
 		// if it's any error other than service not found, it's Real
90 90
 		if !client.IsErrServiceNotFound(err) {
... ...
@@ -85,7 +85,7 @@ func ServiceProgress(ctx context.Context, client client.APIClient, serviceID str
85 85
 	)
86 86
 
87 87
 	for {
88
-		service, _, err := client.ServiceInspectWithRaw(ctx, serviceID)
88
+		service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
89 89
 		if err != nil {
90 90
 			return err
91 91
 		}
... ...
@@ -71,7 +71,7 @@ func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale uint6
71 71
 	client := dockerCli.Client()
72 72
 	ctx := context.Background()
73 73
 
74
-	service, _, err := client.ServiceInspectWithRaw(ctx, serviceID)
74
+	service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
75 75
 	if err != nil {
76 76
 		return err
77 77
 	}
... ...
@@ -101,7 +101,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *service
101 101
 	apiClient := dockerCli.Client()
102 102
 	ctx := context.Background()
103 103
 
104
-	service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID)
104
+	service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
105 105
 	if err != nil {
106 106
 		return err
107 107
 	}
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"fmt"
5 5
 	"strings"
6 6
 
7
+	"github.com/docker/docker/api/types"
7 8
 	"github.com/docker/docker/cli"
8 9
 	"github.com/docker/docker/cli/command"
9 10
 	"github.com/docker/docker/cli/command/inspect"
... ...
@@ -79,7 +80,8 @@ func inspectNode(ctx context.Context, dockerCli *command.DockerCli) inspect.GetR
79 79
 
80 80
 func inspectService(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc {
81 81
 	return func(ref string) (interface{}, []byte, error) {
82
-		return dockerCli.Client().ServiceInspectWithRaw(ctx, ref)
82
+		// Service inspect shows defaults values in empty fields.
83
+		return dockerCli.Client().ServiceInspectWithRaw(ctx, ref, types.ServiceInspectOptions{InsertDefaults: true})
83 84
 	}
84 85
 }
85 86
 
... ...
@@ -123,7 +123,7 @@ type PluginAPIClient interface {
123 123
 // ServiceAPIClient defines API client methods for the services
124 124
 type ServiceAPIClient interface {
125 125
 	ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error)
126
-	ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error)
126
+	ServiceInspectWithRaw(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error)
127 127
 	ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error)
128 128
 	ServiceRemove(ctx context.Context, serviceID string) error
129 129
 	ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error)
... ...
@@ -3,16 +3,21 @@ package client
3 3
 import (
4 4
 	"bytes"
5 5
 	"encoding/json"
6
+	"fmt"
6 7
 	"io/ioutil"
7 8
 	"net/http"
9
+	"net/url"
8 10
 
11
+	"github.com/docker/docker/api/types"
9 12
 	"github.com/docker/docker/api/types/swarm"
10 13
 	"golang.org/x/net/context"
11 14
 )
12 15
 
13 16
 // ServiceInspectWithRaw returns the service information and the raw data.
14
-func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error) {
15
-	serverResp, err := cli.get(ctx, "/services/"+serviceID, nil, nil)
17
+func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string, opts types.ServiceInspectOptions) (swarm.Service, []byte, error) {
18
+	query := url.Values{}
19
+	query.Set("insertDefaults", fmt.Sprintf("%v", opts.InsertDefaults))
20
+	serverResp, err := cli.get(ctx, "/services/"+serviceID, query, nil)
16 21
 	if err != nil {
17 22
 		if serverResp.statusCode == http.StatusNotFound {
18 23
 			return swarm.Service{}, nil, serviceNotFoundError{serviceID}
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"strings"
10 10
 	"testing"
11 11
 
12
+	"github.com/docker/docker/api/types"
12 13
 	"github.com/docker/docker/api/types/swarm"
13 14
 	"golang.org/x/net/context"
14 15
 )
... ...
@@ -18,7 +19,7 @@ func TestServiceInspectError(t *testing.T) {
18 18
 		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
19 19
 	}
20 20
 
21
-	_, _, err := client.ServiceInspectWithRaw(context.Background(), "nothing")
21
+	_, _, err := client.ServiceInspectWithRaw(context.Background(), "nothing", types.ServiceInspectOptions{})
22 22
 	if err == nil || err.Error() != "Error response from daemon: Server error" {
23 23
 		t.Fatalf("expected a Server Error, got %v", err)
24 24
 	}
... ...
@@ -29,7 +30,7 @@ func TestServiceInspectServiceNotFound(t *testing.T) {
29 29
 		client: newMockClient(errorMock(http.StatusNotFound, "Server error")),
30 30
 	}
31 31
 
32
-	_, _, err := client.ServiceInspectWithRaw(context.Background(), "unknown")
32
+	_, _, err := client.ServiceInspectWithRaw(context.Background(), "unknown", types.ServiceInspectOptions{})
33 33
 	if err == nil || !IsErrServiceNotFound(err) {
34 34
 		t.Fatalf("expected a serviceNotFoundError error, got %v", err)
35 35
 	}
... ...
@@ -55,7 +56,7 @@ func TestServiceInspect(t *testing.T) {
55 55
 		}),
56 56
 	}
57 57
 
58
-	serviceInspect, _, err := client.ServiceInspectWithRaw(context.Background(), "service_id")
58
+	serviceInspect, _, err := client.ServiceInspectWithRaw(context.Background(), "service_id", types.ServiceInspectOptions{})
59 59
 	if err != nil {
60 60
 		t.Fatal(err)
61 61
 	}
... ...
@@ -58,9 +58,9 @@ func getNode(ctx context.Context, c swarmapi.ControlClient, input string) (*swar
58 58
 	return rl.Nodes[0], nil
59 59
 }
60 60
 
61
-func getService(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Service, error) {
61
+func getService(ctx context.Context, c swarmapi.ControlClient, input string, insertDefaults bool) (*swarmapi.Service, error) {
62 62
 	// GetService to match via full ID.
63
-	if rg, err := c.GetService(ctx, &swarmapi.GetServiceRequest{ServiceID: input}); err == nil {
63
+	if rg, err := c.GetService(ctx, &swarmapi.GetServiceRequest{ServiceID: input, InsertDefaults: insertDefaults}); err == nil {
64 64
 		return rg.Service, nil
65 65
 	}
66 66
 
... ...
@@ -91,7 +91,15 @@ func getService(ctx context.Context, c swarmapi.ControlClient, input string) (*s
91 91
 		return nil, fmt.Errorf("service %s is ambiguous (%d matches found)", input, l)
92 92
 	}
93 93
 
94
-	return rl.Services[0], nil
94
+	if !insertDefaults {
95
+		return rl.Services[0], nil
96
+	}
97
+
98
+	rg, err := c.GetService(ctx, &swarmapi.GetServiceRequest{ServiceID: rl.Services[0].ID, InsertDefaults: true})
99
+	if err == nil {
100
+		return rg.Service, nil
101
+	}
102
+	return nil, err
95 103
 }
96 104
 
97 105
 func getTask(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Task, error) {
... ...
@@ -87,10 +87,10 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
87 87
 }
88 88
 
89 89
 // GetService returns a service based on an ID or name.
90
-func (c *Cluster) GetService(input string) (types.Service, error) {
90
+func (c *Cluster) GetService(input string, insertDefaults bool) (types.Service, error) {
91 91
 	var service *swarmapi.Service
92 92
 	if err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
93
-		s, err := getService(ctx, state.controlClient, input)
93
+		s, err := getService(ctx, state.controlClient, input, insertDefaults)
94 94
 		if err != nil {
95 95
 			return err
96 96
 		}
... ...
@@ -187,7 +187,7 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
187 187
 			return apierrors.NewBadRequestError(err)
188 188
 		}
189 189
 
190
-		currentService, err := getService(ctx, state.controlClient, serviceIDOrName)
190
+		currentService, err := getService(ctx, state.controlClient, serviceIDOrName, false)
191 191
 		if err != nil {
192 192
 			return err
193 193
 		}
... ...
@@ -289,7 +289,7 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
289 289
 // RemoveService removes a service from a managed swarm cluster.
290 290
 func (c *Cluster) RemoveService(input string) error {
291 291
 	return c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
292
-		service, err := getService(ctx, state.controlClient, input)
292
+		service, err := getService(ctx, state.controlClient, input, false)
293 293
 		if err != nil {
294 294
 			return err
295 295
 		}
... ...
@@ -442,7 +442,7 @@ func convertSelector(ctx context.Context, cc swarmapi.ControlClient, selector *b
442 442
 	// don't rely on swarmkit to resolve IDs, do it ourselves
443 443
 	swarmSelector := &swarmapi.LogSelector{}
444 444
 	for _, s := range selector.Services {
445
-		service, err := getService(ctx, cc, s)
445
+		service, err := getService(ctx, cc, s, false)
446 446
 		if err != nil {
447 447
 			return nil, err
448 448
 		}
... ...
@@ -23,7 +23,7 @@ func (c *Cluster) GetTasks(options apitypes.TaskListOptions) ([]types.Task, erro
23 23
 		if filter.Include("service") {
24 24
 			serviceFilters := filter.Get("service")
25 25
 			for _, serviceFilter := range serviceFilters {
26
-				service, err := c.GetService(serviceFilter)
26
+				service, err := c.GetService(serviceFilter, false)
27 27
 				if err != nil {
28 28
 					return err
29 29
 				}
... ...
@@ -60,6 +60,16 @@ func (s *DockerSwarmSuite) TestAPISwarmServicesCreate(c *check.C) {
60 60
 	id := d.CreateService(c, simpleTestService, setInstances(instances))
61 61
 	waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, instances)
62 62
 
63
+	// insertDefaults inserts UpdateConfig when service is fetched by ID
64
+	_, out, err := d.SockRequest("GET", "/services/"+id+"?insertDefaults=true", nil)
65
+	c.Assert(err, checker.IsNil, check.Commentf("%s", out))
66
+	c.Assert(string(out), checker.Contains, "UpdateConfig")
67
+
68
+	// insertDefaults inserts UpdateConfig when service is fetched by ID
69
+	_, out, err = d.SockRequest("GET", "/services/top?insertDefaults=true", nil)
70
+	c.Assert(err, checker.IsNil, check.Commentf("%s", out))
71
+	c.Assert(string(out), checker.Contains, "UpdateConfig")
72
+
63 73
 	service := d.GetService(c, id)
64 74
 	instances = 5
65 75
 	d.UpdateService(c, service, setInstances(instances))