Signed-off-by: Nishant Totla <nishanttotla@gmail.com>
| ... | ... |
@@ -1,6 +1,7 @@ |
| 1 | 1 |
package cluster |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "encoding/base64" |
|
| 4 | 5 |
"encoding/json" |
| 5 | 6 |
"fmt" |
| 6 | 7 |
"io/ioutil" |
| ... | ... |
@@ -26,6 +27,7 @@ import ( |
| 26 | 26 |
"github.com/docker/docker/opts" |
| 27 | 27 |
"github.com/docker/docker/pkg/ioutils" |
| 28 | 28 |
"github.com/docker/docker/pkg/signal" |
| 29 |
+ "github.com/docker/docker/reference" |
|
| 29 | 30 |
"github.com/docker/docker/runconfig" |
| 30 | 31 |
swarmapi "github.com/docker/swarmkit/api" |
| 31 | 32 |
swarmnode "github.com/docker/swarmkit/node" |
| ... | ... |
@@ -871,6 +873,46 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv |
| 871 | 871 |
return services, nil |
| 872 | 872 |
} |
| 873 | 873 |
|
| 874 |
+// imageWithDigestString takes an image such as name or name:tag |
|
| 875 |
+// and returns the image pinned to a digest, such as name@sha256:34234... |
|
| 876 |
+func (c *Cluster) imageWithDigestString(ctx context.Context, image string, authConfig *apitypes.AuthConfig) (string, error) {
|
|
| 877 |
+ ref, err := reference.ParseNamed(image) |
|
| 878 |
+ if err != nil {
|
|
| 879 |
+ return "", err |
|
| 880 |
+ } |
|
| 881 |
+ // only query registry if not a canonical reference (i.e. with digest) |
|
| 882 |
+ if _, ok := ref.(reference.Canonical); !ok {
|
|
| 883 |
+ ref = reference.WithDefaultTag(ref) |
|
| 884 |
+ |
|
| 885 |
+ namedTaggedRef, ok := ref.(reference.NamedTagged) |
|
| 886 |
+ if !ok {
|
|
| 887 |
+ return "", fmt.Errorf("unable to cast image to NamedTagged reference object")
|
|
| 888 |
+ } |
|
| 889 |
+ |
|
| 890 |
+ repo, _, err := c.config.Backend.GetRepository(ctx, namedTaggedRef, authConfig) |
|
| 891 |
+ if err != nil {
|
|
| 892 |
+ return "", err |
|
| 893 |
+ } |
|
| 894 |
+ dscrptr, err := repo.Tags(ctx).Get(ctx, namedTaggedRef.Tag()) |
|
| 895 |
+ if err != nil {
|
|
| 896 |
+ return "", err |
|
| 897 |
+ } |
|
| 898 |
+ |
|
| 899 |
+ // TODO(nishanttotla): Currently, the service would lose the tag while calling WithDigest |
|
| 900 |
+ // To prevent this, we create the image string manually, which is a bad idea in general |
|
| 901 |
+ // This will be fixed when https://github.com/docker/distribution/pull/2044 is vendored |
|
| 902 |
+ // namedDigestedRef, err := reference.WithDigest(ref, dscrptr.Digest) |
|
| 903 |
+ // if err != nil {
|
|
| 904 |
+ // return "", err |
|
| 905 |
+ // } |
|
| 906 |
+ // return namedDigestedRef.String(), nil |
|
| 907 |
+ return image + "@" + dscrptr.Digest.String(), nil |
|
| 908 |
+ } else {
|
|
| 909 |
+ // reference already contains a digest, so just return it |
|
| 910 |
+ return ref.String(), nil |
|
| 911 |
+ } |
|
| 912 |
+} |
|
| 913 |
+ |
|
| 874 | 914 |
// CreateService creates a new service in a managed swarm cluster. |
| 875 | 915 |
func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) {
|
| 876 | 916 |
c.RLock() |
| ... | ... |
@@ -893,14 +935,33 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string |
| 893 | 893 |
return "", err |
| 894 | 894 |
} |
| 895 | 895 |
|
| 896 |
+ ctnr := serviceSpec.Task.GetContainer() |
|
| 897 |
+ if ctnr == nil {
|
|
| 898 |
+ return "", fmt.Errorf("service does not use container tasks")
|
|
| 899 |
+ } |
|
| 900 |
+ |
|
| 896 | 901 |
if encodedAuth != "" {
|
| 897 |
- ctnr := serviceSpec.Task.GetContainer() |
|
| 898 |
- if ctnr == nil {
|
|
| 899 |
- return "", fmt.Errorf("service does not use container tasks")
|
|
| 900 |
- } |
|
| 901 | 902 |
ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
| 902 | 903 |
} |
| 903 | 904 |
|
| 905 |
+ // retrieve auth config from encoded auth |
|
| 906 |
+ authConfig := &apitypes.AuthConfig{}
|
|
| 907 |
+ if encodedAuth != "" {
|
|
| 908 |
+ if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
|
|
| 909 |
+ logrus.Warnf("invalid authconfig: %v", err)
|
|
| 910 |
+ } |
|
| 911 |
+ } |
|
| 912 |
+ // pin image by digest |
|
| 913 |
+ if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
|
|
| 914 |
+ digestImage, err := c.imageWithDigestString(ctx, ctnr.Image, authConfig) |
|
| 915 |
+ if err != nil {
|
|
| 916 |
+ logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error())
|
|
| 917 |
+ } else {
|
|
| 918 |
+ logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage)
|
|
| 919 |
+ ctnr.Image = digestImage |
|
| 920 |
+ } |
|
| 921 |
+ } |
|
| 922 |
+ |
|
| 904 | 923 |
r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
|
| 905 | 924 |
if err != nil {
|
| 906 | 925 |
return "", err |
| ... | ... |
@@ -955,12 +1016,13 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ |
| 955 | 955 |
return err |
| 956 | 956 |
} |
| 957 | 957 |
|
| 958 |
+ newCtnr := serviceSpec.Task.GetContainer() |
|
| 959 |
+ if newCtnr == nil {
|
|
| 960 |
+ return fmt.Errorf("service does not use container tasks")
|
|
| 961 |
+ } |
|
| 962 |
+ |
|
| 958 | 963 |
if encodedAuth != "" {
|
| 959 |
- ctnr := serviceSpec.Task.GetContainer() |
|
| 960 |
- if ctnr == nil {
|
|
| 961 |
- return fmt.Errorf("service does not use container tasks")
|
|
| 962 |
- } |
|
| 963 |
- ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
|
| 964 |
+ newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
|
| 964 | 965 |
} else {
|
| 965 | 966 |
// this is needed because if the encodedAuth isn't being updated then we |
| 966 | 967 |
// shouldn't lose it, and continue to use the one that was already present |
| ... | ... |
@@ -979,7 +1041,29 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ |
| 979 | 979 |
if ctnr == nil {
|
| 980 | 980 |
return fmt.Errorf("service does not use container tasks")
|
| 981 | 981 |
} |
| 982 |
- serviceSpec.Task.GetContainer().PullOptions = ctnr.PullOptions |
|
| 982 |
+ newCtnr.PullOptions = ctnr.PullOptions |
|
| 983 |
+ // update encodedAuth so it can be used to pin image by digest |
|
| 984 |
+ if ctnr.PullOptions != nil {
|
|
| 985 |
+ encodedAuth = ctnr.PullOptions.RegistryAuth |
|
| 986 |
+ } |
|
| 987 |
+ } |
|
| 988 |
+ |
|
| 989 |
+ // retrieve auth config from encoded auth |
|
| 990 |
+ authConfig := &apitypes.AuthConfig{}
|
|
| 991 |
+ if encodedAuth != "" {
|
|
| 992 |
+ if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
|
|
| 993 |
+ logrus.Warnf("invalid authconfig: %v", err)
|
|
| 994 |
+ } |
|
| 995 |
+ } |
|
| 996 |
+ // pin image by digest |
|
| 997 |
+ if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
|
|
| 998 |
+ digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig) |
|
| 999 |
+ if err != nil {
|
|
| 1000 |
+ logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error())
|
|
| 1001 |
+ } else if newCtnr.Image != digestImage {
|
|
| 1002 |
+ logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
|
|
| 1003 |
+ newCtnr.Image = digestImage |
|
| 1004 |
+ } |
|
| 983 | 1005 |
} |
| 984 | 1006 |
|
| 985 | 1007 |
_, err = c.client.UpdateService( |
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"io" |
| 5 | 5 |
"time" |
| 6 | 6 |
|
| 7 |
+ "github.com/docker/distribution" |
|
| 7 | 8 |
"github.com/docker/docker/api/types" |
| 8 | 9 |
"github.com/docker/docker/api/types/container" |
| 9 | 10 |
"github.com/docker/docker/api/types/events" |
| ... | ... |
@@ -45,5 +46,5 @@ type Backend interface {
|
| 45 | 45 |
UnsubscribeFromEvents(listener chan interface{})
|
| 46 | 46 |
UpdateAttachment(string, string, string, *network.NetworkingConfig) error |
| 47 | 47 |
WaitForDetachment(context.Context, string, string, string, string) error |
| 48 |
- ResolveTagToDigest(context.Context, reference.NamedTagged, *types.AuthConfig) (string, error) |
|
| 48 |
+ GetRepository(context.Context, reference.NamedTagged, *types.AuthConfig) (distribution.Repository, bool, error) |
|
| 49 | 49 |
} |
| ... | ... |
@@ -4,6 +4,7 @@ import ( |
| 4 | 4 |
"io" |
| 5 | 5 |
"strings" |
| 6 | 6 |
|
| 7 |
+ dist "github.com/docker/distribution" |
|
| 7 | 8 |
"github.com/docker/distribution/digest" |
| 8 | 9 |
"github.com/docker/docker/api/types" |
| 9 | 10 |
"github.com/docker/docker/builder" |
| ... | ... |
@@ -105,40 +106,39 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference. |
| 105 | 105 |
return err |
| 106 | 106 |
} |
| 107 | 107 |
|
| 108 |
-func (daemon *Daemon) ResolveTagToDigest(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig) (string, error) {
|
|
| 108 |
+func (daemon *Daemon) GetRepository(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig) (dist.Repository, bool, error) {
|
|
| 109 | 109 |
// get repository info |
| 110 | 110 |
repoInfo, err := daemon.RegistryService.ResolveRepository(ref) |
| 111 | 111 |
if err != nil {
|
| 112 |
- return "", err |
|
| 112 |
+ return nil, false, err |
|
| 113 | 113 |
} |
| 114 | 114 |
// makes sure name is not empty or `scratch` |
| 115 | 115 |
if err := distribution.ValidateRepoName(repoInfo.Name()); err != nil {
|
| 116 |
- return "", err |
|
| 116 |
+ return nil, false, err |
|
| 117 | 117 |
} |
| 118 | 118 |
|
| 119 | 119 |
// get endpoints |
| 120 | 120 |
endpoints, err := daemon.RegistryService.LookupPullEndpoints(repoInfo.Hostname()) |
| 121 | 121 |
if err != nil {
|
| 122 |
- return "", err |
|
| 122 |
+ return nil, false, err |
|
| 123 | 123 |
} |
| 124 | 124 |
|
| 125 | 125 |
// retrieve repository |
| 126 |
- // TODO(nishanttotla): More sophisticated selection of endpoint |
|
| 127 |
- repo, confirmedV2, err := distribution.NewV2Repository(ctx, repoInfo, endpoints[0], nil, authConfig, "pull") |
|
| 128 |
- |
|
| 129 |
- if err != nil {
|
|
| 130 |
- return "", err |
|
| 131 |
- } |
|
| 132 |
- digest := "" |
|
| 126 |
+ var ( |
|
| 127 |
+ confirmedV2 bool |
|
| 128 |
+ repository dist.Repository |
|
| 129 |
+ lastError error |
|
| 130 |
+ ) |
|
| 131 |
+ |
|
| 132 |
+ for _, endpoint := range endpoints {
|
|
| 133 |
+ if endpoint.Version == registry.APIVersion1 {
|
|
| 134 |
+ continue |
|
| 135 |
+ } |
|
| 133 | 136 |
|
| 134 |
- // only retrieve digest if the repo is v2 |
|
| 135 |
- if confirmedV2 {
|
|
| 136 |
- dscrptr, err := repo.Tags(ctx).Get(ctx, ref.Tag()) |
|
| 137 |
- if err != nil {
|
|
| 138 |
- return "", err |
|
| 139 |
- } else {
|
|
| 140 |
- digest = dscrptr.Digest.String() |
|
| 137 |
+ repository, confirmedV2, lastError = distribution.NewV2Repository(ctx, repoInfo, endpoint, nil, authConfig, "pull") |
|
| 138 |
+ if lastError == nil && confirmedV2 {
|
|
| 139 |
+ break |
|
| 141 | 140 |
} |
| 142 | 141 |
} |
| 143 |
- return digest, nil |
|
| 142 |
+ return repository, confirmedV2, lastError |
|
| 144 | 143 |
} |