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