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