From c1635c1ae3459e41fea3adb8188d9b3ce94f21a0 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Wed, 5 Apr 2017 15:43:17 -0700 Subject: [PATCH] Moving docker service digest pinning to client side Signed-off-by: Nishant Totla --- api/types/client.go | 12 ++++++++ client/distribution_inspect.go | 31 ++++++++++++++++++++ client/interface.go | 6 ++++ client/service_create.go | 52 +++++++++++++++++++++++++++++++++- client/service_update.go | 20 +++++++++++++ client/utils.go | 3 +- 6 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 client/distribution_inspect.go diff --git a/api/types/client.go b/api/types/client.go index 00255271b2..0ce2c94303 100644 --- a/api/types/client.go +++ b/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. diff --git a/client/distribution_inspect.go b/client/distribution_inspect.go new file mode 100644 index 0000000000..d17ab73213 --- /dev/null +++ b/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 +} diff --git a/client/interface.go b/client/interface.go index bccfc7b2ca..7f749bc4b1 100644 --- a/client/interface.go +++ b/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) diff --git a/client/service_create.go b/client/service_create.go index 3d1be225bd..a88d1fba3e 100644 --- a/client/service_create.go +++ b/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) +} diff --git a/client/service_update.go b/client/service_update.go index 873a1e0556..e3f93ee86e 100644 --- a/client/service_update.go +++ b/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 } diff --git a/client/utils.go b/client/utils.go index 23d520ecb8..f3d8877df7 100644 --- a/client/utils.go +++ b/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`)