mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
The WithMockClient option was explicitly resetting the client's API version (see [1]), which differs from the regular client, which is initialized with the current API version used by the client (see [2]). This patch: - reduces the `WithMockClient` to only set the custom HTTP client, leaving other fields un-touched. - adds a test utility and updates tests to handle the API-version prefix - removes redundant uses of `WithVersion()` in tests; for most test-cases it was used to make sure a current API version is used that supports the feature being tested, but there was no test to verify the behavior for lower API versions, so we may as well test against "latest". [1]:5a582729d8/client/client_mock_test.go (L22-L36)[2]:5a582729d8/client/client.go (L167-L190)Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
203 lines
6.8 KiB
Go
203 lines
6.8 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
registrytypes "github.com/moby/moby/api/types/registry"
|
|
"github.com/moby/moby/api/types/swarm"
|
|
"github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
func TestServiceCreateError(t *testing.T) {
|
|
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
|
|
assert.NilError(t, err)
|
|
_, err = client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, ServiceCreateOptions{})
|
|
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
|
|
}
|
|
|
|
// TestServiceCreateConnectionError verifies that connection errors occurring
|
|
// during API-version negotiation are not shadowed by API-version errors.
|
|
//
|
|
// Regression test for https://github.com/docker/cli/issues/4890
|
|
func TestServiceCreateConnectionError(t *testing.T) {
|
|
client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
|
|
assert.NilError(t, err)
|
|
|
|
_, err = client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, ServiceCreateOptions{})
|
|
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
|
|
}
|
|
|
|
func TestServiceCreate(t *testing.T) {
|
|
const expectedURL = "/services/create"
|
|
client, err := NewClientWithOpts(WithMockClient(func(req *http.Request) (*http.Response, error) {
|
|
if err := assertRequest(req, http.MethodPost, expectedURL); err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := json.Marshal(swarm.ServiceCreateResponse{
|
|
ID: "service_id",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(bytes.NewReader(b)),
|
|
}, nil
|
|
}))
|
|
assert.NilError(t, err)
|
|
|
|
r, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, ServiceCreateOptions{})
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Equal(r.ID, "service_id"))
|
|
}
|
|
|
|
func TestServiceCreateCompatiblePlatforms(t *testing.T) {
|
|
client, err := NewClientWithOpts(WithMockClient(func(req *http.Request) (*http.Response, error) {
|
|
if strings.HasPrefix(req.URL.Path, defaultAPIPath+"/services/create") {
|
|
var serviceSpec swarm.ServiceSpec
|
|
|
|
// check if the /distribution endpoint returned correct output
|
|
err := json.NewDecoder(req.Body).Decode(&serviceSpec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
assert.Check(t, is.Equal("foobar:1.0@sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96", serviceSpec.TaskTemplate.ContainerSpec.Image))
|
|
assert.Check(t, is.Len(serviceSpec.TaskTemplate.Placement.Platforms, 1))
|
|
|
|
p := serviceSpec.TaskTemplate.Placement.Platforms[0]
|
|
b, err := json.Marshal(swarm.ServiceCreateResponse{
|
|
ID: "service_" + p.OS + "_" + p.Architecture,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(bytes.NewReader(b)),
|
|
}, nil
|
|
} else if strings.HasPrefix(req.URL.Path, defaultAPIPath+"/distribution/") {
|
|
b, err := json.Marshal(registrytypes.DistributionInspect{
|
|
Descriptor: ocispec.Descriptor{
|
|
Digest: "sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96",
|
|
},
|
|
Platforms: []ocispec.Platform{
|
|
{
|
|
Architecture: "amd64",
|
|
OS: "linux",
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(bytes.NewReader(b)),
|
|
}, nil
|
|
} else {
|
|
return nil, fmt.Errorf("unexpected URL '%s'", req.URL.Path)
|
|
}
|
|
}))
|
|
assert.NilError(t, err)
|
|
|
|
spec := swarm.ServiceSpec{TaskTemplate: swarm.TaskSpec{ContainerSpec: &swarm.ContainerSpec{Image: "foobar:1.0"}}}
|
|
|
|
r, err := client.ServiceCreate(context.Background(), spec, ServiceCreateOptions{QueryRegistry: true})
|
|
assert.NilError(t, err)
|
|
assert.Check(t, is.Equal("service_linux_amd64", r.ID))
|
|
}
|
|
|
|
func TestServiceCreateDigestPinning(t *testing.T) {
|
|
dgst := "sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96"
|
|
dgstAlt := "sha256:37ffbf3f7497c07584dc9637ffbf3f7497c0758c0537ffbf3f7497c0c88e2bb7"
|
|
serviceCreateImage := ""
|
|
pinByDigestTests := []struct {
|
|
img string // input image provided by the user
|
|
expected string // expected image after digest pinning
|
|
}{
|
|
// default registry returns familiar string
|
|
{"docker.io/library/alpine", "alpine:latest@" + dgst},
|
|
// provided tag is preserved and digest added
|
|
{"alpine:edge", "alpine:edge@" + dgst},
|
|
// image with provided alternative digest remains unchanged
|
|
{"alpine@" + dgstAlt, "alpine@" + dgstAlt},
|
|
// image with provided tag and alternative digest remains unchanged
|
|
{"alpine:edge@" + dgstAlt, "alpine:edge@" + dgstAlt},
|
|
// image on alternative registry does not result in familiar string
|
|
{"alternate.registry/library/alpine", "alternate.registry/library/alpine:latest@" + dgst},
|
|
// unresolvable image does not get a digest
|
|
{"cannotresolve", "cannotresolve:latest"},
|
|
}
|
|
|
|
client, err := NewClientWithOpts(WithMockClient(func(req *http.Request) (*http.Response, error) {
|
|
if strings.HasPrefix(req.URL.Path, defaultAPIPath+"/services/create") {
|
|
// reset and set image received by the service create endpoint
|
|
serviceCreateImage = ""
|
|
var service swarm.ServiceSpec
|
|
if err := json.NewDecoder(req.Body).Decode(&service); err != nil {
|
|
return nil, errors.New("could not parse service create request")
|
|
}
|
|
serviceCreateImage = service.TaskTemplate.ContainerSpec.Image
|
|
|
|
b, err := json.Marshal(swarm.ServiceCreateResponse{
|
|
ID: "service_id",
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(bytes.NewReader(b)),
|
|
}, nil
|
|
} else if strings.HasPrefix(req.URL.Path, defaultAPIPath+"/distribution/cannotresolve") {
|
|
// unresolvable image
|
|
return nil, errors.New("cannot resolve image")
|
|
} else if strings.HasPrefix(req.URL.Path, defaultAPIPath+"/distribution/") {
|
|
// resolvable images
|
|
b, err := json.Marshal(registrytypes.DistributionInspect{
|
|
Descriptor: ocispec.Descriptor{
|
|
Digest: digest.Digest(dgst),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(bytes.NewReader(b)),
|
|
}, nil
|
|
}
|
|
return nil, fmt.Errorf("unexpected URL '%s'", req.URL.Path)
|
|
}))
|
|
assert.NilError(t, err)
|
|
|
|
// run pin by digest tests
|
|
for _, p := range pinByDigestTests {
|
|
r, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{
|
|
TaskTemplate: swarm.TaskSpec{
|
|
ContainerSpec: &swarm.ContainerSpec{
|
|
Image: p.img,
|
|
},
|
|
},
|
|
}, ServiceCreateOptions{QueryRegistry: true})
|
|
assert.NilError(t, err)
|
|
|
|
assert.Check(t, is.Equal(r.ID, "service_id"))
|
|
|
|
assert.Check(t, is.Equal(p.expected, serviceCreateImage))
|
|
}
|
|
}
|