Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
| ... | ... |
@@ -276,6 +276,12 @@ type ServiceCreateOptions struct {
|
| 276 | 276 |
// |
| 277 | 277 |
// This field follows the format of the X-Registry-Auth header. |
| 278 | 278 |
EncodedRegistryAuth string |
| 279 |
+ |
|
| 280 |
+ // QueryRegistry indicates whether the service update requires |
|
| 281 |
+ // contacting a registry. A registry may be contacted to retrieve |
|
| 282 |
+ // the image digest and manifest, which in turn can be used to update |
|
| 283 |
+ // platform or other information about the service. |
|
| 284 |
+ QueryRegistry bool |
|
| 279 | 285 |
} |
| 280 | 286 |
|
| 281 | 287 |
// ServiceCreateResponse contains the information returned to a client |
| ... | ... |
@@ -315,6 +321,12 @@ type ServiceUpdateOptions struct {
|
| 315 | 315 |
// The valid values are "previous" and "none". An empty value is the |
| 316 | 316 |
// same as "none". |
| 317 | 317 |
Rollback string |
| 318 |
+ |
|
| 319 |
+ // QueryRegistry indicates whether the service update requires |
|
| 320 |
+ // contacting a registry. A registry may be contacted to retrieve |
|
| 321 |
+ // the image digest and manifest, which in turn can be used to update |
|
| 322 |
+ // platform or other information about the service. |
|
| 323 |
+ QueryRegistry bool |
|
| 318 | 324 |
} |
| 319 | 325 |
|
| 320 | 326 |
// ServiceListOptions holds parameters to list services with. |
| 321 | 327 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,31 @@ |
| 0 |
+package client |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "encoding/json" |
|
| 4 |
+ "net/url" |
|
| 5 |
+ |
|
| 6 |
+ registrytypes "github.com/docker/docker/api/types/registry" |
|
| 7 |
+ "golang.org/x/net/context" |
|
| 8 |
+) |
|
| 9 |
+ |
|
| 10 |
+// DistributionInspect returns the image digest with full Manifest |
|
| 11 |
+func (cli *Client) DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registrytypes.DistributionInspect, error) {
|
|
| 12 |
+ var headers map[string][]string |
|
| 13 |
+ |
|
| 14 |
+ if encodedRegistryAuth != "" {
|
|
| 15 |
+ headers = map[string][]string{
|
|
| 16 |
+ "X-Registry-Auth": {encodedRegistryAuth},
|
|
| 17 |
+ } |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ // Contact the registry to retrieve digest and platform information |
|
| 21 |
+ var distributionInspect registrytypes.DistributionInspect |
|
| 22 |
+ resp, err := cli.get(ctx, "/distribution/"+image+"/json", url.Values{}, headers)
|
|
| 23 |
+ if err != nil {
|
|
| 24 |
+ return distributionInspect, err |
|
| 25 |
+ } |
|
| 26 |
+ |
|
| 27 |
+ err = json.NewDecoder(resp.body).Decode(&distributionInspect) |
|
| 28 |
+ ensureReaderClosed(resp) |
|
| 29 |
+ return distributionInspect, err |
|
| 30 |
+} |
| ... | ... |
@@ -20,6 +20,7 @@ import ( |
| 20 | 20 |
type CommonAPIClient interface {
|
| 21 | 21 |
ConfigAPIClient |
| 22 | 22 |
ContainerAPIClient |
| 23 |
+ DistributionAPIClient |
|
| 23 | 24 |
ImageAPIClient |
| 24 | 25 |
NodeAPIClient |
| 25 | 26 |
NetworkAPIClient |
| ... | ... |
@@ -69,6 +70,11 @@ type ContainerAPIClient interface {
|
| 69 | 69 |
ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) |
| 70 | 70 |
} |
| 71 | 71 |
|
| 72 |
+// DistributionAPIClient defines API client methods for the registry |
|
| 73 |
+type DistributionAPIClient interface {
|
|
| 74 |
+ DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registry.DistributionInspect, error) |
|
| 75 |
+} |
|
| 76 |
+ |
|
| 72 | 77 |
// ImageAPIClient defines API client methods for the images |
| 73 | 78 |
type ImageAPIClient interface {
|
| 74 | 79 |
ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) |
| ... | ... |
@@ -2,15 +2,21 @@ package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"encoding/json" |
| 5 |
+ "fmt" |
|
| 5 | 6 |
|
| 7 |
+ "github.com/docker/distribution/reference" |
|
| 6 | 8 |
"github.com/docker/docker/api/types" |
| 7 | 9 |
"github.com/docker/docker/api/types/swarm" |
| 10 |
+ "github.com/opencontainers/go-digest" |
|
| 8 | 11 |
"golang.org/x/net/context" |
| 9 | 12 |
) |
| 10 | 13 |
|
| 11 | 14 |
// ServiceCreate creates a new Service. |
| 12 | 15 |
func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) {
|
| 13 |
- var headers map[string][]string |
|
| 16 |
+ var ( |
|
| 17 |
+ headers map[string][]string |
|
| 18 |
+ distErr error |
|
| 19 |
+ ) |
|
| 14 | 20 |
|
| 15 | 21 |
if options.EncodedRegistryAuth != "" {
|
| 16 | 22 |
headers = map[string][]string{
|
| ... | ... |
@@ -18,6 +24,18 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, |
| 18 | 18 |
} |
| 19 | 19 |
} |
| 20 | 20 |
|
| 21 |
+ // Contact the registry to retrieve digest and platform information |
|
| 22 |
+ if options.QueryRegistry {
|
|
| 23 |
+ distributionInspect, err := cli.DistributionInspect(ctx, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth) |
|
| 24 |
+ distErr = err |
|
| 25 |
+ if err == nil {
|
|
| 26 |
+ // now pin by digest if the image doesn't already contain a digest |
|
| 27 |
+ img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest) |
|
| 28 |
+ if img != "" {
|
|
| 29 |
+ service.TaskTemplate.ContainerSpec.Image = img |
|
| 30 |
+ } |
|
| 31 |
+ } |
|
| 32 |
+ } |
|
| 21 | 33 |
var response types.ServiceCreateResponse |
| 22 | 34 |
resp, err := cli.post(ctx, "/services/create", nil, service, headers) |
| 23 | 35 |
if err != nil {
|
| ... | ... |
@@ -25,6 +43,38 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, |
| 25 | 25 |
} |
| 26 | 26 |
|
| 27 | 27 |
err = json.NewDecoder(resp.body).Decode(&response) |
| 28 |
+ |
|
| 29 |
+ if distErr != nil {
|
|
| 30 |
+ response.Warnings = append(response.Warnings, digestWarning(service.TaskTemplate.ContainerSpec.Image)) |
|
| 31 |
+ } |
|
| 32 |
+ |
|
| 28 | 33 |
ensureReaderClosed(resp) |
| 29 | 34 |
return response, err |
| 30 | 35 |
} |
| 36 |
+ |
|
| 37 |
+// imageWithDigestString takes an image string and a digest, and updates |
|
| 38 |
+// the image string if it didn't originally contain a digest. It assumes |
|
| 39 |
+// that the image string is not an image ID |
|
| 40 |
+func imageWithDigestString(image string, dgst digest.Digest) string {
|
|
| 41 |
+ isCanonical := false |
|
| 42 |
+ ref, err := reference.ParseAnyReference(image) |
|
| 43 |
+ if err == nil {
|
|
| 44 |
+ _, isCanonical = ref.(reference.Canonical) |
|
| 45 |
+ |
|
| 46 |
+ if !isCanonical {
|
|
| 47 |
+ namedRef, _ := ref.(reference.Named) |
|
| 48 |
+ img, err := reference.WithDigest(namedRef, dgst) |
|
| 49 |
+ if err == nil {
|
|
| 50 |
+ return img.String() |
|
| 51 |
+ } |
|
| 52 |
+ } |
|
| 53 |
+ } |
|
| 54 |
+ return "" |
|
| 55 |
+} |
|
| 56 |
+ |
|
| 57 |
+// digestWarning constructs a formatted warning string using the |
|
| 58 |
+// image name that could not be pinned by digest. The formatting |
|
| 59 |
+// is hardcoded, but could me made smarter in the future |
|
| 60 |
+func digestWarning(image string) string {
|
|
| 61 |
+ return fmt.Sprintf("image %s could not be accessed on a registry to record\nits digest. Each node will access %s independently,\npossibly leading to different nodes running different\nversions of the image.\n", image, image)
|
|
| 62 |
+} |
| ... | ... |
@@ -15,6 +15,7 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version |
| 15 | 15 |
var ( |
| 16 | 16 |
headers map[string][]string |
| 17 | 17 |
query = url.Values{}
|
| 18 |
+ distErr error |
|
| 18 | 19 |
) |
| 19 | 20 |
|
| 20 | 21 |
if options.EncodedRegistryAuth != "" {
|
| ... | ... |
@@ -33,6 +34,20 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version |
| 33 | 33 |
|
| 34 | 34 |
query.Set("version", strconv.FormatUint(version.Index, 10))
|
| 35 | 35 |
|
| 36 |
+ // Contact the registry to retrieve digest and platform information |
|
| 37 |
+ // This happens only when the image has changed |
|
| 38 |
+ if options.QueryRegistry {
|
|
| 39 |
+ distributionInspect, err := cli.DistributionInspect(ctx, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth) |
|
| 40 |
+ distErr = err |
|
| 41 |
+ if err == nil {
|
|
| 42 |
+ // now pin by digest if the image doesn't already contain a digest |
|
| 43 |
+ img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest) |
|
| 44 |
+ if img != "" {
|
|
| 45 |
+ service.TaskTemplate.ContainerSpec.Image = img |
|
| 46 |
+ } |
|
| 47 |
+ } |
|
| 48 |
+ } |
|
| 49 |
+ |
|
| 36 | 50 |
var response types.ServiceUpdateResponse |
| 37 | 51 |
resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers) |
| 38 | 52 |
if err != nil {
|
| ... | ... |
@@ -40,6 +55,11 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version |
| 40 | 40 |
} |
| 41 | 41 |
|
| 42 | 42 |
err = json.NewDecoder(resp.body).Decode(&response) |
| 43 |
+ |
|
| 44 |
+ if distErr != nil {
|
|
| 45 |
+ response.Warnings = append(response.Warnings, digestWarning(service.TaskTemplate.ContainerSpec.Image)) |
|
| 46 |
+ } |
|
| 47 |
+ |
|
| 43 | 48 |
ensureReaderClosed(resp) |
| 44 | 49 |
return response, err |
| 45 | 50 |
} |