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>

Aaron Lehmann authored on 2016/11/15 11:08:24
Showing 19 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
 		}
... ...
@@ -1050,12 +1050,12 @@ func (c *Cluster) imageWithDigestString(ctx context.Context, image string, authC
1050 1050
 }
1051 1051
 
1052 1052
 // CreateService creates a new service in a managed swarm cluster.
1053
-func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) {
1053
+func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apitypes.ServiceCreateResponse, error) {
1054 1054
 	c.RLock()
1055 1055
 	defer c.RUnlock()
1056 1056
 
1057 1057
 	if !c.isActiveManager() {
1058
-		return "", c.errNoManager()
1058
+		return nil, c.errNoManager()
1059 1059
 	}
1060 1060
 
1061 1061
 	ctx, cancel := c.getRequestContext()
... ...
@@ -1063,17 +1063,17 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string
1063 1063
 
1064 1064
 	err := c.populateNetworkID(ctx, c.client, &s)
1065 1065
 	if err != nil {
1066
-		return "", err
1066
+		return nil, err
1067 1067
 	}
1068 1068
 
1069 1069
 	serviceSpec, err := convert.ServiceSpecToGRPC(s)
1070 1070
 	if err != nil {
1071
-		return "", err
1071
+		return nil, err
1072 1072
 	}
1073 1073
 
1074 1074
 	ctnr := serviceSpec.Task.GetContainer()
1075 1075
 	if ctnr == nil {
1076
-		return "", fmt.Errorf("service does not use container tasks")
1076
+		return nil, fmt.Errorf("service does not use container tasks")
1077 1077
 	}
1078 1078
 
1079 1079
 	if encodedAuth != "" {
... ...
@@ -1087,11 +1087,15 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string
1087 1087
 			logrus.Warnf("invalid authconfig: %v", err)
1088 1088
 		}
1089 1089
 	}
1090
+
1091
+	resp := &apitypes.ServiceCreateResponse{}
1092
+
1090 1093
 	// pin image by digest
1091 1094
 	if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
1092 1095
 		digestImage, err := c.imageWithDigestString(ctx, ctnr.Image, authConfig)
1093 1096
 		if err != nil {
1094 1097
 			logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error())
1098
+			resp.Warnings = append(resp.Warnings, fmt.Sprintf("unable to pin image %s to digest: %s", ctnr.Image, err.Error()))
1095 1099
 		} else {
1096 1100
 			logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage)
1097 1101
 			ctnr.Image = digestImage
... ...
@@ -1100,10 +1104,11 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string
1100 1100
 
1101 1101
 	r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
1102 1102
 	if err != nil {
1103
-		return "", err
1103
+		return nil, err
1104 1104
 	}
1105 1105
 
1106
-	return r.Service.ID, nil
1106
+	resp.ID = r.Service.ID
1107
+	return resp, nil
1107 1108
 }
1108 1109
 
1109 1110
 // GetService returns a service based on an ID or name.
... ...
@@ -1126,12 +1131,12 @@ func (c *Cluster) GetService(input string) (types.Service, error) {
1126 1126
 }
1127 1127
 
1128 1128
 // UpdateService updates existing service to match new properties.
1129
-func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, encodedAuth string, registryAuthFrom string) error {
1129
+func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, encodedAuth string, registryAuthFrom string) (*apitypes.ServiceUpdateResponse, error) {
1130 1130
 	c.RLock()
1131 1131
 	defer c.RUnlock()
1132 1132
 
1133 1133
 	if !c.isActiveManager() {
1134
-		return c.errNoManager()
1134
+		return nil, c.errNoManager()
1135 1135
 	}
1136 1136
 
1137 1137
 	ctx, cancel := c.getRequestContext()
... ...
@@ -1139,22 +1144,22 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1139 1139
 
1140 1140
 	err := c.populateNetworkID(ctx, c.client, &spec)
1141 1141
 	if err != nil {
1142
-		return err
1142
+		return nil, err
1143 1143
 	}
1144 1144
 
1145 1145
 	serviceSpec, err := convert.ServiceSpecToGRPC(spec)
1146 1146
 	if err != nil {
1147
-		return err
1147
+		return nil, err
1148 1148
 	}
1149 1149
 
1150 1150
 	currentService, err := getService(ctx, c.client, serviceIDOrName)
1151 1151
 	if err != nil {
1152
-		return err
1152
+		return nil, err
1153 1153
 	}
1154 1154
 
1155 1155
 	newCtnr := serviceSpec.Task.GetContainer()
1156 1156
 	if newCtnr == nil {
1157
-		return fmt.Errorf("service does not use container tasks")
1157
+		return nil, fmt.Errorf("service does not use container tasks")
1158 1158
 	}
1159 1159
 
1160 1160
 	if encodedAuth != "" {
... ...
@@ -1168,14 +1173,14 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1168 1168
 			ctnr = currentService.Spec.Task.GetContainer()
1169 1169
 		case apitypes.RegistryAuthFromPreviousSpec:
1170 1170
 			if currentService.PreviousSpec == nil {
1171
-				return fmt.Errorf("service does not have a previous spec")
1171
+				return nil, fmt.Errorf("service does not have a previous spec")
1172 1172
 			}
1173 1173
 			ctnr = currentService.PreviousSpec.Task.GetContainer()
1174 1174
 		default:
1175
-			return fmt.Errorf("unsupported registryAuthFromValue")
1175
+			return nil, fmt.Errorf("unsupported registryAuthFromValue")
1176 1176
 		}
1177 1177
 		if ctnr == nil {
1178
-			return fmt.Errorf("service does not use container tasks")
1178
+			return nil, fmt.Errorf("service does not use container tasks")
1179 1179
 		}
1180 1180
 		newCtnr.PullOptions = ctnr.PullOptions
1181 1181
 		// update encodedAuth so it can be used to pin image by digest
... ...
@@ -1191,11 +1196,15 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1191 1191
 			logrus.Warnf("invalid authconfig: %v", err)
1192 1192
 		}
1193 1193
 	}
1194
+
1195
+	resp := &apitypes.ServiceUpdateResponse{}
1196
+
1194 1197
 	// pin image by digest
1195 1198
 	if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
1196 1199
 		digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig)
1197 1200
 		if err != nil {
1198 1201
 			logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error())
1202
+			resp.Warnings = append(resp.Warnings, fmt.Sprintf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error()))
1199 1203
 		} else if newCtnr.Image != digestImage {
1200 1204
 			logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
1201 1205
 			newCtnr.Image = digestImage
... ...
@@ -1212,7 +1221,8 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
1212 1212
 			},
1213 1213
 		},
1214 1214
 	)
1215
-	return err
1215
+
1216
+	return resp, err
1216 1217
 }
1217 1218
 
1218 1219
 // RemoveService removes a service from a managed swarm cluster.
... ...
@@ -171,6 +171,7 @@ This section lists each version from latest to oldest.  Each listing includes a
171 171
 * `POST /containers/create` now takes `StopTimeout` field.
172 172
 * `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.
173 173
 * `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.
174
+* `POST /services/create` and `POST /services/(id or name)/update` now return a `Warnings` array.
174 175
 * `GET /networks/(name)` now returns field `Created` in response to show network created time.
175 176
 * `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.
176 177
 * `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`
... ...
@@ -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 \