1package client // import "github.com/docker/docker/client" 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 9 "github.com/docker/distribution/reference" 10 "github.com/docker/docker/api/types" 11 "github.com/docker/docker/api/types/swarm" 12 digest "github.com/opencontainers/go-digest" 13 "github.com/pkg/errors" 14) 15 16// ServiceCreate creates a new Service. 17func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) { 18 var response types.ServiceCreateResponse 19 headers := map[string][]string{ 20 "version": {cli.version}, 21 } 22 23 if options.EncodedRegistryAuth != "" { 24 headers["X-Registry-Auth"] = []string{options.EncodedRegistryAuth} 25 } 26 27 // Make sure containerSpec is not nil when no runtime is set or the runtime is set to container 28 if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) { 29 service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{} 30 } 31 32 if err := validateServiceSpec(service); err != nil { 33 return response, err 34 } 35 36 // ensure that the image is tagged 37 var resolveWarning string 38 switch { 39 case service.TaskTemplate.ContainerSpec != nil: 40 if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" { 41 service.TaskTemplate.ContainerSpec.Image = taggedImg 42 } 43 if options.QueryRegistry { 44 resolveWarning = resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) 45 } 46 case service.TaskTemplate.PluginSpec != nil: 47 if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" { 48 service.TaskTemplate.PluginSpec.Remote = taggedImg 49 } 50 if options.QueryRegistry { 51 resolveWarning = resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) 52 } 53 } 54 55 resp, err := cli.post(ctx, "/services/create", nil, service, headers) 56 defer ensureReaderClosed(resp) 57 if err != nil { 58 return response, err 59 } 60 61 err = json.NewDecoder(resp.body).Decode(&response) 62 if resolveWarning != "" { 63 response.Warnings = append(response.Warnings, resolveWarning) 64 } 65 66 return response, err 67} 68 69func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { 70 var warning string 71 if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth); err != nil { 72 warning = digestWarning(taskSpec.ContainerSpec.Image) 73 } else { 74 taskSpec.ContainerSpec.Image = img 75 if len(imgPlatforms) > 0 { 76 if taskSpec.Placement == nil { 77 taskSpec.Placement = &swarm.Placement{} 78 } 79 taskSpec.Placement.Platforms = imgPlatforms 80 } 81 } 82 return warning 83} 84 85func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { 86 var warning string 87 if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth); err != nil { 88 warning = digestWarning(taskSpec.PluginSpec.Remote) 89 } else { 90 taskSpec.PluginSpec.Remote = img 91 if len(imgPlatforms) > 0 { 92 if taskSpec.Placement == nil { 93 taskSpec.Placement = &swarm.Placement{} 94 } 95 taskSpec.Placement.Platforms = imgPlatforms 96 } 97 } 98 return warning 99} 100 101func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) { 102 distributionInspect, err := cli.DistributionInspect(ctx, image, encodedAuth) 103 var platforms []swarm.Platform 104 if err != nil { 105 return "", nil, err 106 } 107 108 imageWithDigest := imageWithDigestString(image, distributionInspect.Descriptor.Digest) 109 110 if len(distributionInspect.Platforms) > 0 { 111 platforms = make([]swarm.Platform, 0, len(distributionInspect.Platforms)) 112 for _, p := range distributionInspect.Platforms { 113 // clear architecture field for arm. This is a temporary patch to address 114 // https://github.com/docker/swarmkit/issues/2294. The issue is that while 115 // image manifests report "arm" as the architecture, the node reports 116 // something like "armv7l" (includes the variant), which causes arm images 117 // to stop working with swarm mode. This patch removes the architecture 118 // constraint for arm images to ensure tasks get scheduled. 119 arch := p.Architecture 120 if strings.ToLower(arch) == "arm" { 121 arch = "" 122 } 123 platforms = append(platforms, swarm.Platform{ 124 Architecture: arch, 125 OS: p.OS, 126 }) 127 } 128 } 129 return imageWithDigest, platforms, err 130} 131 132// imageWithDigestString takes an image string and a digest, and updates 133// the image string if it didn't originally contain a digest. It returns 134// image unmodified in other situations. 135func imageWithDigestString(image string, dgst digest.Digest) string { 136 namedRef, err := reference.ParseNormalizedNamed(image) 137 if err == nil { 138 if _, isCanonical := namedRef.(reference.Canonical); !isCanonical { 139 // ensure that image gets a default tag if none is provided 140 img, err := reference.WithDigest(namedRef, dgst) 141 if err == nil { 142 return reference.FamiliarString(img) 143 } 144 } 145 } 146 return image 147} 148 149// imageWithTagString takes an image string, and returns a tagged image 150// string, adding a 'latest' tag if one was not provided. It returns an 151// empty string if a canonical reference was provided 152func imageWithTagString(image string) string { 153 namedRef, err := reference.ParseNormalizedNamed(image) 154 if err == nil { 155 return reference.FamiliarString(reference.TagNameOnly(namedRef)) 156 } 157 return "" 158} 159 160// digestWarning constructs a formatted warning string using the 161// image name that could not be pinned by digest. The formatting 162// is hardcoded, but could me made smarter in the future 163func digestWarning(image string) string { 164 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) 165} 166 167func validateServiceSpec(s swarm.ServiceSpec) error { 168 if s.TaskTemplate.ContainerSpec != nil && s.TaskTemplate.PluginSpec != nil { 169 return errors.New("must not specify both a container spec and a plugin spec in the task template") 170 } 171 if s.TaskTemplate.PluginSpec != nil && s.TaskTemplate.Runtime != swarm.RuntimePlugin { 172 return errors.New("mismatched runtime with plugin spec") 173 } 174 if s.TaskTemplate.ContainerSpec != nil && (s.TaskTemplate.Runtime != "" && s.TaskTemplate.Runtime != swarm.RuntimeContainer) { 175 return errors.New("mismatched runtime with container spec") 176 } 177 return nil 178} 179