github.com/ssdev-go/moby@v17.12.1-ce-rc2+incompatible/client/service_create.go (about)

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