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