Browse code

Return warnings from service create and service update when digest pinning fails

Modify the service update and create APIs to return optional warning
messages as part of the response. Populate these messages with an
informative reason when digest resolution fails.

This is a small API change, but significantly improves the UX. The user
can now get immediate feedback when they've specified a nonexistent
image or unreachable registry.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
(cherry picked from commit 948e60691e523022f88e7f8129f02106a0f8826c)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>

Aaron Lehmann authored on 2016/11/15 11:08:24
Showing 18 changed files
... ...
@@ -18,8 +18,8 @@ type Backend interface {
18 18
 	UnlockSwarm(req types.UnlockRequest) error
19 19
 	GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
20 20
 	GetService(string) (types.Service, error)
21
-	CreateService(types.ServiceSpec, string) (string, error)
22
-	UpdateService(string, uint64, types.ServiceSpec, string, string) error
21
+	CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error)
22
+	UpdateService(string, uint64, types.ServiceSpec, string, string) (*basictypes.ServiceUpdateResponse, error)
23 23
 	RemoveService(string) error
24 24
 	ServiceLogs(context.Context, string, *backend.ContainerLogsConfig, chan struct{}) error
25 25
 	GetNodes(basictypes.NodeListOptions) ([]types.Node, error)
... ...
@@ -166,15 +166,13 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter,
166 166
 	// Get returns "" if the header does not exist
167 167
 	encodedAuth := r.Header.Get("X-Registry-Auth")
168 168
 
169
-	id, err := sr.backend.CreateService(service, encodedAuth)
169
+	resp, err := sr.backend.CreateService(service, encodedAuth)
170 170
 	if err != nil {
171 171
 		logrus.Errorf("Error creating service %s: %v", service.Name, err)
172 172
 		return err
173 173
 	}
174 174
 
175
-	return httputils.WriteJSON(w, http.StatusCreated, &basictypes.ServiceCreateResponse{
176
-		ID: id,
177
-	})
175
+	return httputils.WriteJSON(w, http.StatusCreated, resp)
178 176
 }
179 177
 
180 178
 func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
... ...
@@ -194,11 +192,12 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
194 194
 
195 195
 	registryAuthFrom := r.URL.Query().Get("registryAuthFrom")
196 196
 
197
-	if err := sr.backend.UpdateService(vars["id"], version, service, encodedAuth, registryAuthFrom); err != nil {
197
+	resp, err := sr.backend.UpdateService(vars["id"], version, service, encodedAuth, registryAuthFrom)
198
+	if err != nil {
198 199
 		logrus.Errorf("Error updating service %s: %v", vars["id"], err)
199 200
 		return err
200 201
 	}
201
-	return nil
202
+	return httputils.WriteJSON(w, http.StatusOK, resp)
202 203
 }
203 204
 
204 205
 func (sr *swarmRouter) removeService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
... ...
@@ -2218,6 +2218,16 @@ definitions:
2218 2218
       Deleted:
2219 2219
         description: "The image ID of an image that was deleted"
2220 2220
         type: "string"
2221
+  ServiceUpdateResponse:
2222
+    type: "object"
2223
+    properties:
2224
+      Warnings:
2225
+        description: "Optional warning messages"
2226
+        type: "array"
2227
+        items:
2228
+          type: "string"
2229
+    example:
2230
+      Warning: "unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found"
2221 2231
   ContainerSummary:
2222 2232
     type: "array"
2223 2233
     items:
... ...
@@ -6875,8 +6885,12 @@ paths:
6875 6875
               ID:
6876 6876
                 description: "The ID of the created service."
6877 6877
                 type: "string"
6878
+              Warning:
6879
+                description: "Optional warning message"
6880
+                type: "string"
6878 6881
             example:
6879 6882
               ID: "ak7w3gjqoa3kuz8xcpnyy0pvl"
6883
+              Warning: "unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found"
6880 6884
         409:
6881 6885
           description: "name conflicts with an existing service"
6882 6886
           schema:
... ...
@@ -6998,10 +7012,14 @@ paths:
6998 6998
   /services/{id}/update:
6999 6999
     post:
7000 7000
       summary: "Update a service"
7001
-      operationId: "PostServicesUpdate"
7001
+      operationId: "ServiceUpdate"
7002
+      consumes: ["application/json"]
7003
+      produces: ["application/json"]
7002 7004
       responses:
7003 7005
         200:
7004 7006
           description: "no error"
7007
+          schema:
7008
+            $ref: "#/definitions/ImageDeleteResponse"
7005 7009
         404:
7006 7010
           description: "no such service"
7007 7011
           schema:
... ...
@@ -7065,8 +7083,7 @@ paths:
7065 7065
           description: "A base64-encoded auth configuration for pulling from private registries. [See the authentication section for details.](#section/Authentication)"
7066 7066
           type: "string"
7067 7067
 
7068
-      tags:
7069
-        - "Services"
7068
+      tags: [Service]
7070 7069
   /tasks:
7071 7070
     get:
7072 7071
       summary: "List tasks"
... ...
@@ -285,10 +285,12 @@ type ServiceCreateOptions struct {
285 285
 }
286 286
 
287 287
 // ServiceCreateResponse contains the information returned to a client
288
-// on the  creation of a new service.
288
+// on the creation of a new service.
289 289
 type ServiceCreateResponse struct {
290 290
 	// ID is the ID of the created service.
291 291
 	ID string
292
+	// Warnings is a set of non-fatal warning messages to pass on to the user.
293
+	Warnings []string `json:",omitempty"`
292 294
 }
293 295
 
294 296
 // Values for RegistryAuthFrom in ServiceUpdateOptions
295 297
new file mode 100644
... ...
@@ -0,0 +1,12 @@
0
+package types
1
+
2
+// This file was generated by the swagger tool.
3
+// Editing this file might prove futile when you re-run the swagger generate command
4
+
5
+// ServiceUpdateResponse service update response
6
+// swagger:model ServiceUpdateResponse
7
+type ServiceUpdateResponse struct {
8
+
9
+	// Optional warning messages
10
+	Warnings []string `json:"Warnings"`
11
+}
... ...
@@ -11,7 +11,7 @@ type Volume struct {
11 11
 	// Required: true
12 12
 	Driver string `json:"Driver"`
13 13
 
14
-	// A mapping of abitrary key/value data set on this volume.
14
+	// User-defined key/value metadata.
15 15
 	// Required: true
16 16
 	Labels map[string]string `json:"Labels"`
17 17
 
... ...
@@ -19,7 +19,7 @@ type VolumesCreateBody struct {
19 19
 	// Required: true
20 20
 	DriverOpts map[string]string `json:"DriverOpts"`
21 21
 
22
-	// A mapping of arbitrary key/value data to set on the volume.
22
+	// User-defined key/value metadata.
23 23
 	// Required: true
24 24
 	Labels map[string]string `json:"Labels"`
25 25
 
... ...
@@ -90,6 +90,10 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
90 90
 		return err
91 91
 	}
92 92
 
93
+	for _, warning := range response.Warnings {
94
+		fmt.Fprintln(dockerCli.Err(), warning)
95
+	}
96
+
93 97
 	fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
94 98
 	return nil
95 99
 }
... ...
@@ -82,11 +82,15 @@ func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale uint6
82 82
 
83 83
 	serviceMode.Replicated.Replicas = &scale
84 84
 
85
-	err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, types.ServiceUpdateOptions{})
85
+	response, err := client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, types.ServiceUpdateOptions{})
86 86
 	if err != nil {
87 87
 		return err
88 88
 	}
89 89
 
90
+	for _, warning := range response.Warnings {
91
+		fmt.Fprintln(dockerCli.Err(), warning)
92
+	}
93
+
90 94
 	fmt.Fprintf(dockerCli.Out(), "%s scaled to %d\n", serviceID, scale)
91 95
 	return nil
92 96
 }
... ...
@@ -133,11 +133,15 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str
133 133
 		updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec
134 134
 	}
135 135
 
136
-	err = apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts)
136
+	response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts)
137 137
 	if err != nil {
138 138
 		return err
139 139
 	}
140 140
 
141
+	for _, warning := range response.Warnings {
142
+		fmt.Fprintln(dockerCli.Err(), warning)
143
+	}
144
+
141 145
 	fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
142 146
 	return nil
143 147
 }
... ...
@@ -408,15 +408,20 @@ func deployServices(
408 408
 			if sendAuth {
409 409
 				updateOpts.EncodedRegistryAuth = encodedAuth
410 410
 			}
411
-			if err := apiClient.ServiceUpdate(
411
+			response, err := apiClient.ServiceUpdate(
412 412
 				ctx,
413 413
 				service.ID,
414 414
 				service.Version,
415 415
 				serviceSpec,
416 416
 				updateOpts,
417
-			); err != nil {
417
+			)
418
+			if err != nil {
418 419
 				return err
419 420
 			}
421
+
422
+			for _, warning := range response.Warnings {
423
+				fmt.Fprintln(dockerCli.Err(), warning)
424
+			}
420 425
 		} else {
421 426
 			fmt.Fprintf(out, "Creating service %s\n", name)
422 427
 
... ...
@@ -124,7 +124,7 @@ type ServiceAPIClient interface {
124 124
 	ServiceInspectWithRaw(ctx context.Context, serviceID string) (swarm.Service, []byte, error)
125 125
 	ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error)
126 126
 	ServiceRemove(ctx context.Context, serviceID string) error
127
-	ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error
127
+	ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error)
128 128
 	ServiceLogs(ctx context.Context, serviceID string, options types.ContainerLogsOptions) (io.ReadCloser, error)
129 129
 	TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error)
130 130
 	TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error)
... ...
@@ -1,6 +1,7 @@
1 1
 package client
2 2
 
3 3
 import (
4
+	"encoding/json"
4 5
 	"net/url"
5 6
 	"strconv"
6 7
 
... ...
@@ -10,7 +11,7 @@ import (
10 10
 )
11 11
 
12 12
 // ServiceUpdate updates a Service.
13
-func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) error {
13
+func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) {
14 14
 	var (
15 15
 		headers map[string][]string
16 16
 		query   = url.Values{}
... ...
@@ -28,7 +29,13 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
28 28
 
29 29
 	query.Set("version", strconv.FormatUint(version.Index, 10))
30 30
 
31
+	var response types.ServiceUpdateResponse
31 32
 	resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers)
33
+	if err != nil {
34
+		return response, err
35
+	}
36
+
37
+	err = json.NewDecoder(resp.body).Decode(&response)
32 38
 	ensureReaderClosed(resp)
33
-	return err
39
+	return response, err
34 40
 }
... ...
@@ -19,7 +19,7 @@ func TestServiceUpdateError(t *testing.T) {
19 19
 		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
20 20
 	}
21 21
 
22
-	err := client.ServiceUpdate(context.Background(), "service_id", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{})
22
+	_, err := client.ServiceUpdate(context.Background(), "service_id", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{})
23 23
 	if err == nil || err.Error() != "Error response from daemon: Server error" {
24 24
 		t.Fatalf("expected a Server Error, got %v", err)
25 25
 	}
... ...
@@ -64,12 +64,12 @@ func TestServiceUpdate(t *testing.T) {
64 64
 				}
65 65
 				return &http.Response{
66 66
 					StatusCode: http.StatusOK,
67
-					Body:       ioutil.NopCloser(bytes.NewReader([]byte("body"))),
67
+					Body:       ioutil.NopCloser(bytes.NewReader([]byte("{}"))),
68 68
 				}, nil
69 69
 			}),
70 70
 		}
71 71
 
72
-		err := client.ServiceUpdate(context.Background(), "service_id", updateCase.swarmVersion, swarm.ServiceSpec{}, types.ServiceUpdateOptions{})
72
+		_, err := client.ServiceUpdate(context.Background(), "service_id", updateCase.swarmVersion, swarm.ServiceSpec{}, types.ServiceUpdateOptions{})
73 73
 		if err != nil {
74 74
 			t.Fatal(err)
75 75
 		}
... ...
@@ -1062,12 +1062,12 @@ func (c *Cluster) imageWithDigestString(ctx context.Context, image string, authC
1062 1062
 }
1063 1063
 
1064 1064
 // CreateService creates a new service in a managed swarm cluster.
1065
-func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) {
1065
+func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apitypes.ServiceCreateResponse, error) {
1066 1066
 	c.RLock()
1067 1067
 	defer c.RUnlock()
1068 1068
 
1069 1069
 	if !c.isActiveManager() {
1070
-		return "", c.errNoManager()
1070
+		return nil, c.errNoManager()
1071 1071
 	}
1072 1072
 
1073 1073
 	ctx, cancel := c.getRequestContext()
... ...
@@ -1075,17 +1075,17 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string
1075 1075
 
1076 1076
 	err := c.populateNetworkID(ctx, c.client, &s)
1077 1077
 	if err != nil {
1078
-		return "", err
1078
+		return nil, err
1079 1079
 	}
1080 1080
 
1081 1081
 	serviceSpec, err := convert.ServiceSpecToGRPC(s)
1082 1082
 	if err != nil {
1083
-		return "", err
1083
+		return nil, err
1084 1084
 	}
1085 1085
 
1086 1086
 	ctnr := serviceSpec.Task.GetContainer()
1087 1087
 	if ctnr == nil {
1088
-		return "", fmt.Errorf("service does not use container tasks")
1088
+		return nil, fmt.Errorf("service does not use container tasks")
1089 1089
 	}
1090 1090
 
1091 1091
 	if encodedAuth != "" {
... ...
@@ -1099,11 +1099,15 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string
1099 1099
 			logrus.Warnf("invalid authconfig: %v", err)
1100 1100
 		}
1101 1101
 	}
1102
+
1103
+	resp := &apitypes.ServiceCreateResponse{}
1104
+
1102 1105
 	// pin image by digest
1103 1106
 	if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
1104 1107
 		digestImage, err := c.imageWithDigestString(ctx, ctnr.Image, authConfig)
1105 1108
 		if err != nil {
1106 1109
 			logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error())
1110
+			resp.Warnings = append(resp.Warnings, fmt.Sprintf("unable to pin image %s to digest: %s", ctnr.Image, err.Error()))
1107 1111
 		} else {
1108 1112
 			logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage)
1109 1113
 			ctnr.Image = digestImage
... ...
@@ -1112,10 +1116,11 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string
1112 1112
 
1113 1113
 	r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
1114 1114
 	if err != nil {
1115
-		return "", err
1115
+		return nil, err
1116 1116
 	}
1117 1117
 
1118
-	return r.Service.ID, nil
1118
+	resp.ID = r.Service.ID
1119
+	return resp, nil
1119 1120
 }
1120 1121
 
1121 1122
 // GetService returns a service based on an ID or name.
... ...
@@ -1138,12 +1143,12 @@ func (c *Cluster) GetService(input string) (types.Service, error) {
1138 1138
 }
1139 1139
 
1140 1140
 // UpdateService updates existing service to match new properties.
1141
-func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, encodedAuth string, registryAuthFrom string) error {
1141
+func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, encodedAuth string, registryAuthFrom string) (*apitypes.ServiceUpdateResponse, error) {
1142 1142
 	c.RLock()
1143 1143
 	defer c.RUnlock()
1144 1144
 
1145 1145
 	if !c.isActiveManager() {
1146
-		return c.errNoManager()
1146
+		return nil, c.errNoManager()
1147 1147
 	}
1148 1148
 
1149 1149
 	ctx, cancel := c.getRequestContext()
... ...
@@ -1151,22 +1156,22 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1151 1151
 
1152 1152
 	err := c.populateNetworkID(ctx, c.client, &spec)
1153 1153
 	if err != nil {
1154
-		return err
1154
+		return nil, err
1155 1155
 	}
1156 1156
 
1157 1157
 	serviceSpec, err := convert.ServiceSpecToGRPC(spec)
1158 1158
 	if err != nil {
1159
-		return err
1159
+		return nil, err
1160 1160
 	}
1161 1161
 
1162 1162
 	currentService, err := getService(ctx, c.client, serviceIDOrName)
1163 1163
 	if err != nil {
1164
-		return err
1164
+		return nil, err
1165 1165
 	}
1166 1166
 
1167 1167
 	newCtnr := serviceSpec.Task.GetContainer()
1168 1168
 	if newCtnr == nil {
1169
-		return fmt.Errorf("service does not use container tasks")
1169
+		return nil, fmt.Errorf("service does not use container tasks")
1170 1170
 	}
1171 1171
 
1172 1172
 	if encodedAuth != "" {
... ...
@@ -1180,14 +1185,14 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1180 1180
 			ctnr = currentService.Spec.Task.GetContainer()
1181 1181
 		case apitypes.RegistryAuthFromPreviousSpec:
1182 1182
 			if currentService.PreviousSpec == nil {
1183
-				return fmt.Errorf("service does not have a previous spec")
1183
+				return nil, fmt.Errorf("service does not have a previous spec")
1184 1184
 			}
1185 1185
 			ctnr = currentService.PreviousSpec.Task.GetContainer()
1186 1186
 		default:
1187
-			return fmt.Errorf("unsupported registryAuthFromValue")
1187
+			return nil, fmt.Errorf("unsupported registryAuthFromValue")
1188 1188
 		}
1189 1189
 		if ctnr == nil {
1190
-			return fmt.Errorf("service does not use container tasks")
1190
+			return nil, fmt.Errorf("service does not use container tasks")
1191 1191
 		}
1192 1192
 		newCtnr.PullOptions = ctnr.PullOptions
1193 1193
 		// update encodedAuth so it can be used to pin image by digest
... ...
@@ -1203,11 +1208,15 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1203 1203
 			logrus.Warnf("invalid authconfig: %v", err)
1204 1204
 		}
1205 1205
 	}
1206
+
1207
+	resp := &apitypes.ServiceUpdateResponse{}
1208
+
1206 1209
 	// pin image by digest
1207 1210
 	if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
1208 1211
 		digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig)
1209 1212
 		if err != nil {
1210 1213
 			logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error())
1214
+			resp.Warnings = append(resp.Warnings, fmt.Sprintf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error()))
1211 1215
 		} else if newCtnr.Image != digestImage {
1212 1216
 			logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
1213 1217
 			newCtnr.Image = digestImage
... ...
@@ -1224,7 +1233,8 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1224 1224
 			},
1225 1225
 		},
1226 1226
 	)
1227
-	return err
1227
+
1228
+	return resp, err
1228 1229
 }
1229 1230
 
1230 1231
 // RemoveService removes a service from a managed swarm cluster.
... ...
@@ -165,6 +165,7 @@ This section lists each version from latest to oldest.  Each listing includes a
165 165
 * `POST /containers/create` now takes `StopTimeout` field.
166 166
 * `POST /services/create` and `POST /services/(id or name)/update` now accept `Monitor` and `MaxFailureRatio` parameters, which control the response to failures during service updates.
167 167
 * `POST /services/(id or name)/update` now accepts a `ForceUpdate` parameter inside the `TaskTemplate`, which causes the service to be updated even if there are no changes which would ordinarily trigger an update.
168
+* `POST /services/create` and `POST /services/(id or name)/update` now return a `Warnings` array.
168 169
 * `GET /networks/(name)` now returns field `Created` in response to show network created time.
169 170
 * `POST /containers/(id or name)/exec` now accepts an `Env` field, which holds a list of environment variables to be set in the context of the command execution.
170 171
 * `GET /volumes`, `GET /volumes/(name)`, and `POST /volumes/create` now return the `Options` field which holds the driver specific options to use for when creating the volume.
... ...
@@ -5262,7 +5262,8 @@ image](#create-an-image) section for more details.
5262 5262
     Content-Type: application/json
5263 5263
 
5264 5264
     {
5265
-      "ID":"ak7w3gjqoa3kuz8xcpnyy0pvl"
5265
+      "ID": "ak7w3gjqoa3kuz8xcpnyy0pvl",
5266
+      "Warnings": ["unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found"]
5266 5267
     }
5267 5268
 
5268 5269
 **Status codes**:
... ...
@@ -5628,6 +5629,16 @@ image](#create-an-image) section for more details.
5628 5628
 -   **404** – no such service
5629 5629
 -   **500** – server error
5630 5630
 
5631
+**Example response**:
5632
+
5633
+    HTTP/1.1 200 OK
5634
+    Content-Type: application/json
5635
+
5636
+    {
5637
+      "Warnings": ["unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found"]
5638
+    }
5639
+
5640
+
5631 5641
 ### Get service logs
5632 5642
 
5633 5643
 `GET /services/(id or name)/logs`
... ...
@@ -8,7 +8,8 @@ swagger generate model -f api/swagger.yaml \
8 8
     -n ImageSummary \
9 9
     -n Plugin -n PluginDevice -n PluginMount -n PluginEnv -n PluginInterfaceType \
10 10
     -n ErrorResponse \
11
-    -n IdResponse
11
+    -n IdResponse \
12
+    -n ServiceUpdateResponse
12 13
 
13 14
 swagger generate operation -f api/swagger.yaml \
14 15
     -t api -a types -m types -C api/swagger-gen.yaml \