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