Files
moby/client/service_create.go
Sebastiaan van Stijn 75520d1f5b client: resolveContainerSpecImage, resolvePluginSpecRemote: early returns
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-26 19:24:27 +01:00

207 lines
7.4 KiB
Go

package client
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"github.com/distribution/reference"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/swarm"
"github.com/opencontainers/go-digest"
)
// ServiceCreateOptions contains the options to use when creating a service.
type ServiceCreateOptions struct {
Spec swarm.ServiceSpec
// EncodedRegistryAuth is the encoded registry authorization credentials to
// use when updating the service.
//
// This field follows the format of the X-Registry-Auth header.
EncodedRegistryAuth string
// QueryRegistry indicates whether the service update requires
// contacting a registry. A registry may be contacted to retrieve
// the image digest and manifest, which in turn can be used to update
// platform or other information about the service.
QueryRegistry bool
}
// ServiceCreateResult represents the result of creating a service.
type ServiceCreateResult struct {
// ID is the ID of the created service.
ID string
// Warnings is a list of warnings that occurred during service creation.
Warnings []string
}
// ServiceCreate creates a new service.
func (cli *Client) ServiceCreate(ctx context.Context, options ServiceCreateOptions) (ServiceCreateResult, error) {
// Make sure containerSpec is not nil when no runtime is set or the runtime is set to container
if options.Spec.TaskTemplate.ContainerSpec == nil && (options.Spec.TaskTemplate.Runtime == "" || options.Spec.TaskTemplate.Runtime == swarm.RuntimeContainer) {
options.Spec.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
}
if err := validateServiceSpec(options.Spec); err != nil {
return ServiceCreateResult{}, err
}
// ensure that the image is tagged
var warnings []string
switch {
case options.Spec.TaskTemplate.ContainerSpec != nil:
if taggedImg := imageWithTagString(options.Spec.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
options.Spec.TaskTemplate.ContainerSpec.Image = taggedImg
}
if options.QueryRegistry {
if warning := resolveContainerSpecImage(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, warning)
}
}
case options.Spec.TaskTemplate.PluginSpec != nil:
if taggedImg := imageWithTagString(options.Spec.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
options.Spec.TaskTemplate.PluginSpec.Remote = taggedImg
}
if options.QueryRegistry {
if warning := resolvePluginSpecRemote(ctx, cli, &options.Spec.TaskTemplate, options.EncodedRegistryAuth); warning != "" {
warnings = append(warnings, warning)
}
}
}
headers := http.Header{}
if options.EncodedRegistryAuth != "" {
headers[registry.AuthHeader] = []string{options.EncodedRegistryAuth}
}
resp, err := cli.post(ctx, "/services/create", nil, options.Spec, headers)
defer ensureReaderClosed(resp)
if err != nil {
return ServiceCreateResult{}, err
}
var response swarm.ServiceCreateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
warnings = append(warnings, response.Warnings...)
return ServiceCreateResult{
ID: response.ID,
Warnings: warnings,
}, err
}
func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth)
if err != nil {
return digestWarning(taskSpec.ContainerSpec.Image)
}
taskSpec.ContainerSpec.Image = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
return ""
}
func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth)
if err != nil {
return digestWarning(taskSpec.PluginSpec.Remote)
}
taskSpec.PluginSpec.Remote = img
if len(imgPlatforms) > 0 {
if taskSpec.Placement == nil {
taskSpec.Placement = &swarm.Placement{}
}
taskSpec.Placement.Platforms = imgPlatforms
}
return ""
}
func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) {
distributionInspect, err := cli.DistributionInspect(ctx, image, DistributionInspectOptions{
EncodedRegistryAuth: encodedAuth,
})
var platforms []swarm.Platform
if err != nil {
return "", nil, err
}
imageWithDigest := imageWithDigestString(image, distributionInspect.Descriptor.Digest)
if len(distributionInspect.Platforms) > 0 {
platforms = make([]swarm.Platform, 0, len(distributionInspect.Platforms))
for _, p := range distributionInspect.Platforms {
// clear architecture field for arm. This is a temporary patch to address
// https://github.com/docker/swarmkit/issues/2294. The issue is that while
// image manifests report "arm" as the architecture, the node reports
// something like "armv7l" (includes the variant), which causes arm images
// to stop working with swarm mode. This patch removes the architecture
// constraint for arm images to ensure tasks get scheduled.
arch := p.Architecture
if strings.ToLower(arch) == "arm" {
arch = ""
}
platforms = append(platforms, swarm.Platform{
Architecture: arch,
OS: p.OS,
})
}
}
return imageWithDigest, platforms, err
}
// imageWithDigestString takes an image string and a digest, and updates
// the image string if it didn't originally contain a digest. It returns
// image unmodified in other situations.
func imageWithDigestString(image string, dgst digest.Digest) string {
namedRef, err := reference.ParseNormalizedNamed(image)
if err == nil {
if _, hasDigest := namedRef.(reference.Digested); !hasDigest {
// ensure that image gets a default tag if none is provided
img, err := reference.WithDigest(namedRef, dgst)
if err == nil {
return reference.FamiliarString(img)
}
}
}
return image
}
// imageWithTagString takes an image string, and returns a tagged image
// string, adding a 'latest' tag if one was not provided. It returns an
// empty string if a canonical reference was provided
func imageWithTagString(image string) string {
namedRef, err := reference.ParseNormalizedNamed(image)
if err == nil {
return reference.FamiliarString(reference.TagNameOnly(namedRef))
}
return ""
}
// digestWarning constructs a formatted warning string using the
// image name that could not be pinned by digest. The formatting
// is hardcoded, but could me made smarter in the future
func digestWarning(image string) string {
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)
}
func validateServiceSpec(s swarm.ServiceSpec) error {
if s.TaskTemplate.ContainerSpec != nil && s.TaskTemplate.PluginSpec != nil {
return errors.New("must not specify both a container spec and a plugin spec in the task template")
}
if s.TaskTemplate.PluginSpec != nil && s.TaskTemplate.Runtime != swarm.RuntimePlugin {
return errors.New("mismatched runtime with plugin spec")
}
if s.TaskTemplate.ContainerSpec != nil && (s.TaskTemplate.Runtime != "" && s.TaskTemplate.Runtime != swarm.RuntimeContainer) {
return errors.New("mismatched runtime with container spec")
}
return nil
}