From c1635c1ae3459e41fea3adb8188d9b3ce94f21a0 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Wed, 5 Apr 2017 15:43:17 -0700 Subject: [PATCH 1/2] 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 00255271b28d9..0ce2c94303bc6 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 0000000000000..d17ab7321364d --- /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 bccfc7b2ca1f9..7f749bc4b13e8 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 3d1be225bddbf..a88d1fba3eeb5 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 873a1e05569e4..e3f93ee86e80a 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 23d520ecb8c7a..f3d8877df79e1 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`) From c0afd9c873183604e57282e1cf47605c1f1e4d43 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Fri, 12 May 2017 13:51:52 -0700 Subject: [PATCH 2/2] Disabling digest pinning for API versions < 1.30 Signed-off-by: Nishant Totla --- api/server/router/swarm/backend.go | 4 ++-- api/server/router/swarm/cluster_routes.go | 15 +++++++++++++-- client/service_create.go | 13 ++++++------- client/service_update.go | 9 +++++---- daemon/cluster/services.go | 18 ++++++++++++------ 5 files changed, 38 insertions(+), 21 deletions(-) diff --git a/api/server/router/swarm/backend.go b/api/server/router/swarm/backend.go index 37ec52957d70c..3b7933d7b1e4b 100644 --- a/api/server/router/swarm/backend.go +++ b/api/server/router/swarm/backend.go @@ -19,8 +19,8 @@ type Backend interface { GetServices(basictypes.ServiceListOptions) ([]types.Service, error) GetService(idOrName string, insertDefaults bool) (types.Service, error) - CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error) - UpdateService(string, uint64, types.ServiceSpec, basictypes.ServiceUpdateOptions) (*basictypes.ServiceUpdateResponse, error) + CreateService(types.ServiceSpec, string, bool) (*basictypes.ServiceCreateResponse, error) + UpdateService(string, uint64, types.ServiceSpec, basictypes.ServiceUpdateOptions, bool) (*basictypes.ServiceUpdateResponse, error) RemoveService(string) error ServiceLogs(context.Context, *backend.LogSelector, *basictypes.ContainerLogsOptions) (<-chan *backend.LogMessage, error) diff --git a/api/server/router/swarm/cluster_routes.go b/api/server/router/swarm/cluster_routes.go index 438f2de705777..91461da764683 100644 --- a/api/server/router/swarm/cluster_routes.go +++ b/api/server/router/swarm/cluster_routes.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/filters" types "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/api/types/versions" "golang.org/x/net/context" ) @@ -178,8 +179,13 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter, // Get returns "" if the header does not exist encodedAuth := r.Header.Get("X-Registry-Auth") + cliVersion := r.Header.Get("version") + queryRegistry := false + if cliVersion != "" && versions.LessThan(cliVersion, "1.30") { + queryRegistry = true + } - resp, err := sr.backend.CreateService(service, encodedAuth) + resp, err := sr.backend.CreateService(service, encodedAuth, queryRegistry) if err != nil { logrus.Errorf("Error creating service %s: %v", service.Name, err) return err @@ -207,8 +213,13 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter, flags.EncodedRegistryAuth = r.Header.Get("X-Registry-Auth") flags.RegistryAuthFrom = r.URL.Query().Get("registryAuthFrom") flags.Rollback = r.URL.Query().Get("rollback") + cliVersion := r.Header.Get("version") + queryRegistry := false + if cliVersion != "" && versions.LessThan(cliVersion, "1.30") { + queryRegistry = true + } - resp, err := sr.backend.UpdateService(vars["id"], version, service, flags) + resp, err := sr.backend.UpdateService(vars["id"], version, service, flags, queryRegistry) if err != nil { logrus.Errorf("Error updating service %s: %v", vars["id"], err) return err diff --git a/client/service_create.go b/client/service_create.go index a88d1fba3eeb5..90082d150fda8 100644 --- a/client/service_create.go +++ b/client/service_create.go @@ -13,15 +13,14 @@ import ( // 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 - distErr error - ) + var distErr error + + headers := map[string][]string{ + "version": {cli.version}, + } if options.EncodedRegistryAuth != "" { - headers = map[string][]string{ - "X-Registry-Auth": {options.EncodedRegistryAuth}, - } + headers["X-Registry-Auth"] = []string{options.EncodedRegistryAuth} } // Contact the registry to retrieve digest and platform information diff --git a/client/service_update.go b/client/service_update.go index e3f93ee86e80a..8c7776aad7385 100644 --- a/client/service_update.go +++ b/client/service_update.go @@ -13,15 +13,16 @@ import ( // ServiceUpdate updates a Service. func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) { var ( - headers map[string][]string query = url.Values{} distErr error ) + headers := map[string][]string{ + "version": {cli.version}, + } + if options.EncodedRegistryAuth != "" { - headers = map[string][]string{ - "X-Registry-Auth": {options.EncodedRegistryAuth}, - } + headers["X-Registry-Auth"] = []string{options.EncodedRegistryAuth} } if options.RegistryAuthFrom != "" { diff --git a/daemon/cluster/services.go b/daemon/cluster/services.go index 0bfc417860252..f4416c24c38ff 100644 --- a/daemon/cluster/services.go +++ b/daemon/cluster/services.go @@ -117,7 +117,7 @@ func (c *Cluster) GetService(input string, insertDefaults bool) (types.Service, } // CreateService creates a new service in a managed swarm cluster. -func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apitypes.ServiceCreateResponse, error) { +func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string, queryRegistry bool) (*apitypes.ServiceCreateResponse, error) { var resp *apitypes.ServiceCreateResponse err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error { err := c.populateNetworkID(ctx, state.controlClient, &s) @@ -151,8 +151,11 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apity } } - // pin image by digest - if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" { + // pin image by digest for API versions < 1.30 + // TODO(nishanttotla): The check on "DOCKER_SERVICE_PREFER_OFFLINE_IMAGE" + // should be removed in the future. Since integration tests only use the + // latest API version, so this is no longer required. + if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" && queryRegistry { digestImage, err := c.imageWithDigestString(ctx, ctnr.Image, authConfig) if err != nil { logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error()) @@ -193,7 +196,7 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apity } // UpdateService updates existing service to match new properties. -func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, flags apitypes.ServiceUpdateOptions) (*apitypes.ServiceUpdateResponse, error) { +func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec types.ServiceSpec, flags apitypes.ServiceUpdateOptions, queryRegistry bool) (*apitypes.ServiceUpdateResponse, error) { var resp *apitypes.ServiceUpdateResponse err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error { @@ -256,8 +259,11 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ resp = &apitypes.ServiceUpdateResponse{} - // pin image by digest - if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" { + // pin image by digest for API versions < 1.30 + // TODO(nishanttotla): The check on "DOCKER_SERVICE_PREFER_OFFLINE_IMAGE" + // should be removed in the future. Since integration tests only use the + // latest API version, so this is no longer required. + if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" && queryRegistry { digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig) if err != nil { logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error())