| ... | ... |
@@ -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" |
| ... | ... |
@@ -24,6 +25,7 @@ import ( |
| 24 | 24 |
"github.com/docker/docker/opts" |
| 25 | 25 |
"github.com/docker/docker/pkg/ioutils" |
| 26 | 26 |
"github.com/docker/docker/pkg/signal" |
| 27 |
+ "github.com/docker/docker/reference" |
|
| 27 | 28 |
"github.com/docker/docker/runconfig" |
| 28 | 29 |
swarmapi "github.com/docker/swarmkit/api" |
| 29 | 30 |
"github.com/docker/swarmkit/manager/encryption" |
| ... | ... |
@@ -974,6 +976,46 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv |
| 974 | 974 |
return services, nil |
| 975 | 975 |
} |
| 976 | 976 |
|
| 977 |
+// imageWithDigestString takes an image such as name or name:tag |
|
| 978 |
+// and returns the image pinned to a digest, such as name@sha256:34234... |
|
| 979 |
+func (c *Cluster) imageWithDigestString(ctx context.Context, image string, authConfig *apitypes.AuthConfig) (string, error) {
|
|
| 980 |
+ ref, err := reference.ParseNamed(image) |
|
| 981 |
+ if err != nil {
|
|
| 982 |
+ return "", err |
|
| 983 |
+ } |
|
| 984 |
+ // only query registry if not a canonical reference (i.e. with digest) |
|
| 985 |
+ if _, ok := ref.(reference.Canonical); !ok {
|
|
| 986 |
+ ref = reference.WithDefaultTag(ref) |
|
| 987 |
+ |
|
| 988 |
+ namedTaggedRef, ok := ref.(reference.NamedTagged) |
|
| 989 |
+ if !ok {
|
|
| 990 |
+ return "", fmt.Errorf("unable to cast image to NamedTagged reference object")
|
|
| 991 |
+ } |
|
| 992 |
+ |
|
| 993 |
+ repo, _, err := c.config.Backend.GetRepository(ctx, namedTaggedRef, authConfig) |
|
| 994 |
+ if err != nil {
|
|
| 995 |
+ return "", err |
|
| 996 |
+ } |
|
| 997 |
+ dscrptr, err := repo.Tags(ctx).Get(ctx, namedTaggedRef.Tag()) |
|
| 998 |
+ if err != nil {
|
|
| 999 |
+ return "", err |
|
| 1000 |
+ } |
|
| 1001 |
+ |
|
| 1002 |
+ // TODO(nishanttotla): Currently, the service would lose the tag while calling WithDigest |
|
| 1003 |
+ // To prevent this, we create the image string manually, which is a bad idea in general |
|
| 1004 |
+ // This will be fixed when https://github.com/docker/distribution/pull/2044 is vendored |
|
| 1005 |
+ // namedDigestedRef, err := reference.WithDigest(ref, dscrptr.Digest) |
|
| 1006 |
+ // if err != nil {
|
|
| 1007 |
+ // return "", err |
|
| 1008 |
+ // } |
|
| 1009 |
+ // return namedDigestedRef.String(), nil |
|
| 1010 |
+ return image + "@" + dscrptr.Digest.String(), nil |
|
| 1011 |
+ } else {
|
|
| 1012 |
+ // reference already contains a digest, so just return it |
|
| 1013 |
+ return ref.String(), nil |
|
| 1014 |
+ } |
|
| 1015 |
+} |
|
| 1016 |
+ |
|
| 977 | 1017 |
// CreateService creates a new service in a managed swarm cluster. |
| 978 | 1018 |
func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) {
|
| 979 | 1019 |
c.RLock() |
| ... | ... |
@@ -996,14 +1038,33 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string |
| 996 | 996 |
return "", err |
| 997 | 997 |
} |
| 998 | 998 |
|
| 999 |
+ ctnr := serviceSpec.Task.GetContainer() |
|
| 1000 |
+ if ctnr == nil {
|
|
| 1001 |
+ return "", fmt.Errorf("service does not use container tasks")
|
|
| 1002 |
+ } |
|
| 1003 |
+ |
|
| 999 | 1004 |
if encodedAuth != "" {
|
| 1000 |
- ctnr := serviceSpec.Task.GetContainer() |
|
| 1001 |
- if ctnr == nil {
|
|
| 1002 |
- return "", fmt.Errorf("service does not use container tasks")
|
|
| 1003 |
- } |
|
| 1004 | 1005 |
ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
| 1005 | 1006 |
} |
| 1006 | 1007 |
|
| 1008 |
+ // retrieve auth config from encoded auth |
|
| 1009 |
+ authConfig := &apitypes.AuthConfig{}
|
|
| 1010 |
+ if encodedAuth != "" {
|
|
| 1011 |
+ if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
|
|
| 1012 |
+ logrus.Warnf("invalid authconfig: %v", err)
|
|
| 1013 |
+ } |
|
| 1014 |
+ } |
|
| 1015 |
+ // pin image by digest |
|
| 1016 |
+ if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
|
|
| 1017 |
+ digestImage, err := c.imageWithDigestString(ctx, ctnr.Image, authConfig) |
|
| 1018 |
+ if err != nil {
|
|
| 1019 |
+ logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error())
|
|
| 1020 |
+ } else {
|
|
| 1021 |
+ logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage)
|
|
| 1022 |
+ ctnr.Image = digestImage |
|
| 1023 |
+ } |
|
| 1024 |
+ } |
|
| 1025 |
+ |
|
| 1007 | 1026 |
r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
|
| 1008 | 1027 |
if err != nil {
|
| 1009 | 1028 |
return "", err |
| ... | ... |
@@ -1058,12 +1119,13 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ |
| 1058 | 1058 |
return err |
| 1059 | 1059 |
} |
| 1060 | 1060 |
|
| 1061 |
+ newCtnr := serviceSpec.Task.GetContainer() |
|
| 1062 |
+ if newCtnr == nil {
|
|
| 1063 |
+ return fmt.Errorf("service does not use container tasks")
|
|
| 1064 |
+ } |
|
| 1065 |
+ |
|
| 1061 | 1066 |
if encodedAuth != "" {
|
| 1062 |
- ctnr := serviceSpec.Task.GetContainer() |
|
| 1063 |
- if ctnr == nil {
|
|
| 1064 |
- return fmt.Errorf("service does not use container tasks")
|
|
| 1065 |
- } |
|
| 1066 |
- ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
|
| 1067 |
+ newCtnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
|
|
| 1067 | 1068 |
} else {
|
| 1068 | 1069 |
// this is needed because if the encodedAuth isn't being updated then we |
| 1069 | 1070 |
// shouldn't lose it, and continue to use the one that was already present |
| ... | ... |
@@ -1082,7 +1144,29 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ |
| 1082 | 1082 |
if ctnr == nil {
|
| 1083 | 1083 |
return fmt.Errorf("service does not use container tasks")
|
| 1084 | 1084 |
} |
| 1085 |
- serviceSpec.Task.GetContainer().PullOptions = ctnr.PullOptions |
|
| 1085 |
+ newCtnr.PullOptions = ctnr.PullOptions |
|
| 1086 |
+ // update encodedAuth so it can be used to pin image by digest |
|
| 1087 |
+ if ctnr.PullOptions != nil {
|
|
| 1088 |
+ encodedAuth = ctnr.PullOptions.RegistryAuth |
|
| 1089 |
+ } |
|
| 1090 |
+ } |
|
| 1091 |
+ |
|
| 1092 |
+ // retrieve auth config from encoded auth |
|
| 1093 |
+ authConfig := &apitypes.AuthConfig{}
|
|
| 1094 |
+ if encodedAuth != "" {
|
|
| 1095 |
+ if err := json.NewDecoder(base64.NewDecoder(base64.URLEncoding, strings.NewReader(encodedAuth))).Decode(authConfig); err != nil {
|
|
| 1096 |
+ logrus.Warnf("invalid authconfig: %v", err)
|
|
| 1097 |
+ } |
|
| 1098 |
+ } |
|
| 1099 |
+ // pin image by digest |
|
| 1100 |
+ if os.Getenv("DOCKER_SERVICE_PREFER_OFFLINE_IMAGE") != "1" {
|
|
| 1101 |
+ digestImage, err := c.imageWithDigestString(ctx, newCtnr.Image, authConfig) |
|
| 1102 |
+ if err != nil {
|
|
| 1103 |
+ logrus.Warnf("unable to pin image %s to digest: %s", newCtnr.Image, err.Error())
|
|
| 1104 |
+ } else if newCtnr.Image != digestImage {
|
|
| 1105 |
+ logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
|
|
| 1106 |
+ newCtnr.Image = digestImage |
|
| 1107 |
+ } |
|
| 1086 | 1108 |
} |
| 1087 | 1109 |
|
| 1088 | 1110 |
_, err = c.client.UpdateService( |
| ... | ... |
@@ -4,12 +4,14 @@ 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" |
| 10 | 11 |
"github.com/docker/docker/api/types/filters" |
| 11 | 12 |
"github.com/docker/docker/api/types/network" |
| 12 | 13 |
clustertypes "github.com/docker/docker/daemon/cluster/provider" |
| 14 |
+ "github.com/docker/docker/reference" |
|
| 13 | 15 |
"github.com/docker/libnetwork" |
| 14 | 16 |
"github.com/docker/libnetwork/cluster" |
| 15 | 17 |
networktypes "github.com/docker/libnetwork/types" |
| ... | ... |
@@ -45,4 +47,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 |
+ GetRepository(context.Context, reference.NamedTagged, *types.AuthConfig) (distribution.Repository, bool, error) |
|
| 48 | 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" |
| ... | ... |
@@ -104,3 +105,40 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference. |
| 104 | 104 |
<-writesDone |
| 105 | 105 |
return err |
| 106 | 106 |
} |
| 107 |
+ |
|
| 108 |
+func (daemon *Daemon) GetRepository(ctx context.Context, ref reference.NamedTagged, authConfig *types.AuthConfig) (dist.Repository, bool, error) {
|
|
| 109 |
+ // get repository info |
|
| 110 |
+ repoInfo, err := daemon.RegistryService.ResolveRepository(ref) |
|
| 111 |
+ if err != nil {
|
|
| 112 |
+ return nil, false, err |
|
| 113 |
+ } |
|
| 114 |
+ // makes sure name is not empty or `scratch` |
|
| 115 |
+ if err := distribution.ValidateRepoName(repoInfo.Name()); err != nil {
|
|
| 116 |
+ return nil, false, err |
|
| 117 |
+ } |
|
| 118 |
+ |
|
| 119 |
+ // get endpoints |
|
| 120 |
+ endpoints, err := daemon.RegistryService.LookupPullEndpoints(repoInfo.Hostname()) |
|
| 121 |
+ if err != nil {
|
|
| 122 |
+ return nil, false, err |
|
| 123 |
+ } |
|
| 124 |
+ |
|
| 125 |
+ // retrieve repository |
|
| 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 |
+ } |
|
| 136 |
+ |
|
| 137 |
+ repository, confirmedV2, lastError = distribution.NewV2Repository(ctx, repoInfo, endpoint, nil, authConfig, "pull") |
|
| 138 |
+ if lastError == nil && confirmedV2 {
|
|
| 139 |
+ break |
|
| 140 |
+ } |
|
| 141 |
+ } |
|
| 142 |
+ return repository, confirmedV2, lastError |
|
| 143 |
+} |