blob: 3969f66f94e648d65c6d37c49b0aff7b6afcc1e9 [file] [edit]
package cluster
import (
"context"
"errors"
"fmt"
"testing"
"github.com/distribution/reference"
"github.com/docker/distribution"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/swarm"
executorpkg "github.com/moby/moby/v2/daemon/cluster/executor"
swarmapi "github.com/moby/swarmkit/v2/api"
"github.com/opencontainers/go-digest"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
type fakeServiceCreator struct {
swarmapi.ControlClient
conflicts int
err error
names []string
}
func (f *fakeServiceCreator) CreateService(_ context.Context, req *swarmapi.CreateServiceRequest, _ ...grpc.CallOption) (*swarmapi.CreateServiceResponse, error) {
name := req.Spec.Annotations.Name
f.names = append(f.names, name)
if len(f.names) <= f.conflicts {
if f.err != nil {
return nil, f.err
}
return nil, status.Errorf(codes.AlreadyExists, "name conflicts with an existing object: service %s already exists", name)
}
return &swarmapi.CreateServiceResponse{Service: &swarmapi.Service{ID: "serviceid"}}, nil
}
type fakeRepository struct {
distribution.Repository
tags distribution.TagService
}
func (r fakeRepository) Tags(context.Context) distribution.TagService {
return r.tags
}
type fakeTagService struct {
distribution.TagService
digest digest.Digest
err error
calls *int
}
func (s fakeTagService) Get(context.Context, string) (distribution.Descriptor, error) {
*s.calls++
return distribution.Descriptor{Digest: s.digest}, s.err
}
type fakeImageBackend struct {
executorpkg.ImageBackend
repos []distribution.Repository
}
func (b fakeImageBackend) GetRepositories(context.Context, reference.Named, *registry.AuthConfig) ([]distribution.Repository, error) {
return b.repos, nil
}
func TestCreateServiceNameConflict(t *testing.T) {
tests := []struct {
doc string
name string
conflicts int
err error
expNames []string
expErr codes.Code
}{
{
doc: "generated name is redrawn on conflict",
conflicts: 1,
expNames: []string{"generated-0", "generated-1"},
},
{
doc: "gives up after six draws",
conflicts: 100,
expNames: []string{"generated-0", "generated-1", "generated-2", "generated-3", "generated-4", "generated-5"},
expErr: codes.AlreadyExists,
},
{
doc: "caller-chosen name is not redrawn",
name: "bold_lichterman",
conflicts: 1,
expNames: []string{"bold_lichterman"},
expErr: codes.AlreadyExists,
},
{
doc: "other errors are not retried",
conflicts: 1,
err: status.Error(codes.InvalidArgument, "invalid spec"),
expNames: []string{"generated-0"},
expErr: codes.InvalidArgument,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
client := &fakeServiceCreator{conflicts: tc.conflicts, err: tc.err}
var generatedImages []string
cluster := &Cluster{
config: Config{GenerateServiceName: func(_ context.Context, retry int, image string) (string, error) {
generatedImages = append(generatedImages, image)
return fmt.Sprintf("generated-%d", retry), nil
}},
nr: &nodeRunner{nodeState: nodeState{controlClient: client}},
}
resp, err := cluster.CreateService(swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: tc.name},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{Image: "image"},
},
}, "", false)
assert.Check(t, is.DeepEqual(client.names, tc.expNames))
if tc.name == "" {
assert.Check(t, is.Len(generatedImages, len(tc.expNames)))
for _, image := range generatedImages {
assert.Check(t, is.Equal(image, "image"))
}
} else {
assert.Check(t, is.Len(generatedImages, 0))
}
assert.Check(t, is.Equal(status.Code(err), tc.expErr))
if tc.expErr == codes.OK {
assert.Check(t, is.Equal(resp.ID, "serviceid"))
}
})
}
}
// TestImageWithDigestString verifies that repository lookups stop at the first
// successful digest resolution and return an error if all lookups fail.
//
// Regression test for https://github.com/moby/moby/issues/53929
func TestImageWithDigestString(t *testing.T) {
dgst := digest.FromString("manifest")
lookupErr := errors.New("lookup failed")
tests := []struct {
doc string
errs []error
expCalls []int
expErr error
}{
{
doc: "mirror succeeds",
errs: []error{nil, lookupErr},
expCalls: []int{1, 0},
},
{
doc: "upstream succeeds",
errs: []error{lookupErr, nil},
expCalls: []int{1, 1},
},
{
doc: "both fail",
errs: []error{lookupErr, lookupErr},
expCalls: []int{1, 1},
expErr: lookupErr,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
calls := make([]int, len(tc.errs))
repos := make([]distribution.Repository, len(tc.errs))
for i, err := range tc.errs {
repos[i] = fakeRepository{
tags: fakeTagService{
digest: dgst,
err: err,
calls: &calls[i],
},
}
}
cluster := &Cluster{
config: Config{ImageBackend: fakeImageBackend{repos: repos}},
}
got, err := cluster.imageWithDigestString(t.Context(), "busybox:latest", nil)
if tc.expErr != nil {
assert.ErrorIs(t, err, tc.expErr)
} else {
assert.NilError(t, err)
assert.Equal(t, got, "busybox:latest@"+dgst.String())
}
assert.DeepEqual(t, calls, tc.expCalls)
})
}
}