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  }