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