1148a24e64
The "reference" package was moved to a separate module, which was extracted
from b9b19409cf
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
184 lines
6.8 KiB
Go
184 lines
6.8 KiB
Go
package client // import "github.com/docker/docker/client"
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/distribution/reference"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/registry"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/docker/docker/api/types/versions"
|
|
"github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// ServiceCreate creates a new service.
|
|
func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options types.ServiceCreateOptions) (types.ServiceCreateResponse, error) {
|
|
var response types.ServiceCreateResponse
|
|
|
|
// Make sure containerSpec is not nil when no runtime is set or the runtime is set to container
|
|
if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) {
|
|
service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{}
|
|
}
|
|
|
|
if err := validateServiceSpec(service); err != nil {
|
|
return response, err
|
|
}
|
|
|
|
// ensure that the image is tagged
|
|
var resolveWarning string
|
|
switch {
|
|
case service.TaskTemplate.ContainerSpec != nil:
|
|
if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" {
|
|
service.TaskTemplate.ContainerSpec.Image = taggedImg
|
|
}
|
|
if options.QueryRegistry {
|
|
resolveWarning = resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
|
|
}
|
|
case service.TaskTemplate.PluginSpec != nil:
|
|
if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" {
|
|
service.TaskTemplate.PluginSpec.Remote = taggedImg
|
|
}
|
|
if options.QueryRegistry {
|
|
resolveWarning = resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth)
|
|
}
|
|
}
|
|
|
|
headers := http.Header{}
|
|
if versions.LessThan(cli.version, "1.30") {
|
|
// the custom "version" header was used by engine API before 20.10
|
|
// (API 1.30) to switch between client- and server-side lookup of
|
|
// image digests.
|
|
headers["version"] = []string{cli.version}
|
|
}
|
|
if options.EncodedRegistryAuth != "" {
|
|
headers[registry.AuthHeader] = []string{options.EncodedRegistryAuth}
|
|
}
|
|
resp, err := cli.post(ctx, "/services/create", nil, service, headers)
|
|
defer ensureReaderClosed(resp)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
|
|
err = json.NewDecoder(resp.body).Decode(&response)
|
|
if resolveWarning != "" {
|
|
response.Warnings = append(response.Warnings, resolveWarning)
|
|
}
|
|
|
|
return response, err
|
|
}
|
|
|
|
func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
|
|
var warning string
|
|
if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth); err != nil {
|
|
warning = digestWarning(taskSpec.ContainerSpec.Image)
|
|
} else {
|
|
taskSpec.ContainerSpec.Image = img
|
|
if len(imgPlatforms) > 0 {
|
|
if taskSpec.Placement == nil {
|
|
taskSpec.Placement = &swarm.Placement{}
|
|
}
|
|
taskSpec.Placement.Platforms = imgPlatforms
|
|
}
|
|
}
|
|
return warning
|
|
}
|
|
|
|
func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string {
|
|
var warning string
|
|
if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth); err != nil {
|
|
warning = digestWarning(taskSpec.PluginSpec.Remote)
|
|
} else {
|
|
taskSpec.PluginSpec.Remote = img
|
|
if len(imgPlatforms) > 0 {
|
|
if taskSpec.Placement == nil {
|
|
taskSpec.Placement = &swarm.Placement{}
|
|
}
|
|
taskSpec.Placement.Platforms = imgPlatforms
|
|
}
|
|
}
|
|
return warning
|
|
}
|
|
|
|
func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) {
|
|
distributionInspect, err := cli.DistributionInspect(ctx, image, 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 _, isCanonical := namedRef.(reference.Canonical); !isCanonical {
|
|
// 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
|
|
}
|