github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/engine/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  	"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.
    17  func (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  
    69  func 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  
    85  func 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  
   101  func 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.
   135  func 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
   152  func 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
   163  func 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  
   167  func 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  }