moby/client/service_create.go
Sebastiaan van Stijn 1148a24e64
migrate to new github.com/distribution/reference module
The "reference" package was moved to a separate module, which was extracted
from b9b19409cf

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-09-05 12:09:26 +02:00

184 lines
6.8 KiB
Go

package client // import "github.com/docker/docker/client"
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// ServiceCreate creates a new service.
func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) {
var response types.ServiceCreateResponse
// Make sure containerSpec is not nil when no runtime is set or the runtime is set to container
if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) {
service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
}
if err := validateServiceSpec(service); err != nil {
return response, err
}
// ensure that the image is tagged
var resolveWarning string
switch {
case service.TaskTemplate.ContainerSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
service.TaskTemplate.ContainerSpec.Image = taggedImg
}
if options.QueryRegistry {
resolveWarning = resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
}
case service.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
service.TaskTemplate.PluginSpec.Remote = taggedImg
}
if options.QueryRegistry {
resolveWarning = resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
}
}
headers := http.Header{}
if versions.LessThan(cli.version, "1.30") {
// the custom "version" header was used by engine API before 20.10
// (API 1.30) to switch between client- and server-side lookup of
// image digests.
headers["version"] = []string{cli.version}
}
if options.EncodedRegistryAuth != "" {
headers[registry.AuthHeader] = []string{options.EncodedRegistryAuth}
}
resp, err := cli.post(ctx, "/services/create", nil, service, headers)
defer ensureReaderClosed(resp)
if err != nil {
return response, err
}
err = json.NewDecoder(resp.body).Decode(&response)
if resolveWarning != "" {
response.Warnings = append(response.Warnings, resolveWarning)
}
return response, err
}
func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
var warning string
if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth); err != nil {
warning = digestWarning(taskSpec.ContainerSpec.Image)
} else {
taskSpec.ContainerSpec.Image = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
}
return warning
}
func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
var warning string
if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth); err != nil {
warning = digestWarning(taskSpec.PluginSpec.Remote)
} else {
taskSpec.PluginSpec.Remote = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
}
return warning
}
func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) {
distributionInspect, err := cli.DistributionInspect(ctx, image, encodedAuth)
var platforms []swarm.Platform
if err != nil {
return "", nil, err
}
imageWithDigest := imageWithDigestString(image, distributionInspect.Descriptor.Digest)
if len(distributionInspect.Platforms) > 0 {
platforms = make([]swarm.Platform, 0, len(distributionInspect.Platforms))
for _, p := range distributionInspect.Platforms {
// clear architecture field for arm. This is a temporary patch to address
// https://github.com/docker/swarmkit/issues/2294. The issue is that while
// image manifests report "arm" as the architecture, the node reports
// something like "armv7l" (includes the variant), which causes arm images
// to stop working with swarm mode. This patch removes the architecture
// constraint for arm images to ensure tasks get scheduled.
arch := p.Architecture
if strings.ToLower(arch) == "arm" {
arch = ""
}
platforms = append(platforms, swarm.Platform{
Architecture: arch,
OS: p.OS,
})
}
}
return imageWithDigest, platforms, err
}
// imageWithDigestString takes an image string and a digest, and updates
// the image string if it didn't originally contain a digest. It returns
// image unmodified in other situations.
func imageWithDigestString(image string, dgst digest.Digest) string {
namedRef, err := reference.ParseNormalizedNamed(image)
if err == nil {
if _, isCanonical := namedRef.(reference.Canonical); !isCanonical {
// ensure that image gets a default tag if none is provided
img, err := reference.WithDigest(namedRef, dgst)
if err == nil {
return reference.FamiliarString(img)
}
}
}
return image
}
// imageWithTagString takes an image string, and returns a tagged image
// string, adding a 'latest' tag if one was not provided. It returns an
// empty string if a canonical reference was provided
func imageWithTagString(image string) string {
namedRef, err := reference.ParseNormalizedNamed(image)
if err == nil {
return reference.FamiliarString(reference.TagNameOnly(namedRef))
}
return ""
}
// digestWarning constructs a formatted warning string using the
// image name that could not be pinned by digest. The formatting
// is hardcoded, but could me made smarter in the future
func digestWarning(image string) string {
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)
}
func validateServiceSpec(s swarm.ServiceSpec) error {
if s.TaskTemplate.ContainerSpec != nil && s.TaskTemplate.PluginSpec != nil {
return errors.New("must not specify both a container spec and a plugin spec in the task template")
}
if s.TaskTemplate.PluginSpec != nil && s.TaskTemplate.Runtime != swarm.RuntimePlugin {
return errors.New("mismatched runtime with plugin spec")
}
if s.TaskTemplate.ContainerSpec != nil && (s.TaskTemplate.Runtime != "" && s.TaskTemplate.Runtime != swarm.RuntimeContainer) {
return errors.New("mismatched runtime with container spec")
}
return nil
}