瀏覽代碼

Moving docker service digest pinning to client side

Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
Nishant Totla 8 年之前
父節點
當前提交
c1635c1ae3
共有 6 個文件被更改,包括 122 次插入2 次删除
  1. 12 0
      api/types/client.go
  2. 31 0
      client/distribution_inspect.go
  3. 6 0
      client/interface.go
  4. 51 1
      client/service_create.go
  5. 20 0
      client/service_update.go
  6. 2 1
      client/utils.go

+ 12 - 0
api/types/client.go

@@ -276,6 +276,12 @@ type ServiceCreateOptions struct {
 	//
 	// This field follows the format of the X-Registry-Auth header.
 	EncodedRegistryAuth string
+
+	// QueryRegistry indicates whether the service update requires
+	// contacting a registry. A registry may be contacted to retrieve
+	// the image digest and manifest, which in turn can be used to update
+	// platform or other information about the service.
+	QueryRegistry bool
 }
 
 // ServiceCreateResponse contains the information returned to a client
@@ -315,6 +321,12 @@ type ServiceUpdateOptions struct {
 	// The valid values are "previous" and "none". An empty value is the
 	// same as "none".
 	Rollback string
+
+	// QueryRegistry indicates whether the service update requires
+	// contacting a registry. A registry may be contacted to retrieve
+	// the image digest and manifest, which in turn can be used to update
+	// platform or other information about the service.
+	QueryRegistry bool
 }
 
 // ServiceListOptions holds parameters to list services with.

+ 31 - 0
client/distribution_inspect.go

@@ -0,0 +1,31 @@
+package client
+
+import (
+	"encoding/json"
+	"net/url"
+
+	registrytypes "github.com/docker/docker/api/types/registry"
+	"golang.org/x/net/context"
+)
+
+// DistributionInspect returns the image digest with full Manifest
+func (cli *Client) DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registrytypes.DistributionInspect, error) {
+	var headers map[string][]string
+
+	if encodedRegistryAuth != "" {
+		headers = map[string][]string{
+			"X-Registry-Auth": {encodedRegistryAuth},
+		}
+	}
+
+	// Contact the registry to retrieve digest and platform information
+	var distributionInspect registrytypes.DistributionInspect
+	resp, err := cli.get(ctx, "/distribution/"+image+"/json", url.Values{}, headers)
+	if err != nil {
+		return distributionInspect, err
+	}
+
+	err = json.NewDecoder(resp.body).Decode(&distributionInspect)
+	ensureReaderClosed(resp)
+	return distributionInspect, err
+}

+ 6 - 0
client/interface.go

@@ -20,6 +20,7 @@ import (
 type CommonAPIClient interface {
 	ConfigAPIClient
 	ContainerAPIClient
+	DistributionAPIClient
 	ImageAPIClient
 	NodeAPIClient
 	NetworkAPIClient
@@ -69,6 +70,11 @@ type ContainerAPIClient interface {
 	ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
 }
 
+// DistributionAPIClient defines API client methods for the registry
+type DistributionAPIClient interface {
+	DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registry.DistributionInspect, error)
+}
+
 // ImageAPIClient defines API client methods for the images
 type ImageAPIClient interface {
 	ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error)

+ 51 - 1
client/service_create.go

@@ -2,15 +2,21 @@ package client
 
 import (
 	"encoding/json"
+	"fmt"
 
+	"github.com/docker/distribution/reference"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/swarm"
+	"github.com/opencontainers/go-digest"
 	"golang.org/x/net/context"
 )
 
 // ServiceCreate creates a new Service.
 func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) {
-	var headers map[string][]string
+	var (
+		headers map[string][]string
+		distErr error
+	)
 
 	if options.EncodedRegistryAuth != "" {
 		headers = map[string][]string{
@@ -18,6 +24,18 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec,
 		}
 	}
 
+	// Contact the registry to retrieve digest and platform information
+	if options.QueryRegistry {
+		distributionInspect, err := cli.DistributionInspect(ctx, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth)
+		distErr = err
+		if err == nil {
+			// now pin by digest if the image doesn't already contain a digest
+			img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest)
+			if img != "" {
+				service.TaskTemplate.ContainerSpec.Image = img
+			}
+		}
+	}
 	var response types.ServiceCreateResponse
 	resp, err := cli.post(ctx, "/services/create", nil, service, headers)
 	if err != nil {
@@ -25,6 +43,38 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec,
 	}
 
 	err = json.NewDecoder(resp.body).Decode(&response)
+
+	if distErr != nil {
+		response.Warnings = append(response.Warnings, digestWarning(service.TaskTemplate.ContainerSpec.Image))
+	}
+
 	ensureReaderClosed(resp)
 	return response, err
 }
+
+// imageWithDigestString takes an image string and a digest, and updates
+// the image string if it didn't originally contain a digest. It assumes
+// that the image string is not an image ID
+func imageWithDigestString(image string, dgst digest.Digest) string {
+	isCanonical := false
+	ref, err := reference.ParseAnyReference(image)
+	if err == nil {
+		_, isCanonical = ref.(reference.Canonical)
+
+		if !isCanonical {
+			namedRef, _ := ref.(reference.Named)
+			img, err := reference.WithDigest(namedRef, dgst)
+			if err == nil {
+				return img.String()
+			}
+		}
+	}
+	return ""
+}
+
+// digestWarning constructs a formatted warning string using the
+// image name that could not be pinned by digest. The formatting
+// is hardcoded, but could me made smarter in the future
+func digestWarning(image string) string {
+	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)
+}

+ 20 - 0
client/service_update.go

@@ -15,6 +15,7 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
 	var (
 		headers map[string][]string
 		query   = url.Values{}
+		distErr error
 	)
 
 	if options.EncodedRegistryAuth != "" {
@@ -33,6 +34,20 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
 
 	query.Set("version", strconv.FormatUint(version.Index, 10))
 
+	// Contact the registry to retrieve digest and platform information
+	// This happens only when the image has changed
+	if options.QueryRegistry {
+		distributionInspect, err := cli.DistributionInspect(ctx, service.TaskTemplate.ContainerSpec.Image, options.EncodedRegistryAuth)
+		distErr = err
+		if err == nil {
+			// now pin by digest if the image doesn't already contain a digest
+			img := imageWithDigestString(service.TaskTemplate.ContainerSpec.Image, distributionInspect.Descriptor.Digest)
+			if img != "" {
+				service.TaskTemplate.ContainerSpec.Image = img
+			}
+		}
+	}
+
 	var response types.ServiceUpdateResponse
 	resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers)
 	if err != nil {
@@ -40,6 +55,11 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
 	}
 
 	err = json.NewDecoder(resp.body).Decode(&response)
+
+	if distErr != nil {
+		response.Warnings = append(response.Warnings, digestWarning(service.TaskTemplate.ContainerSpec.Image))
+	}
+
 	ensureReaderClosed(resp)
 	return response, err
 }

+ 2 - 1
client/utils.go

@@ -1,9 +1,10 @@
 package client
 
 import (
-	"github.com/docker/docker/api/types/filters"
 	"net/url"
 	"regexp"
+
+	"github.com/docker/docker/api/types/filters"
 )
 
 var headerRegexp = regexp.MustCompile(`\ADocker/.+\s\((.+)\)\z`)