94137f6df5
Commite6907243af
applied a fix for situations where the client was configured with API-version negotiation, but did not yet negotiate a version. However, the checkVersion() function that was implemented copied the semantics of cli.NegotiateAPIVersion, which ignored connection failures with the assumption that connection errors would still surface further down. However, when using the result of a failed negotiation for NewVersionError, an API version mismatch error would be produced, masking the actual connection error. This patch changes the signature of checkVersion to return unexpected errors, including failures to connect to the API. Before this patch: docker -H unix:///no/such/socket.sock secret ls "secret list" requires API version 1.25, but the Docker daemon API version is 1.24 With this patch applied: docker -H unix:///no/such/socket.sock secret ls Cannot connect to the Docker daemon at unix:///no/such/socket.sock. Is the docker daemon running? Signed-off-by: Sebastiaan van Stijn <github@gone.nl> (cherry picked from commit6aea26b431
) Conflicts: client/image_list.go client/image_list_test.go Signed-off-by: Bjorn Neergaard <bjorn.neergaard@docker.com>
221 lines
7.1 KiB
Go
221 lines
7.1 KiB
Go
package client // import "github.com/docker/docker/client"
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
registrytypes "github.com/docker/docker/api/types/registry"
|
|
"github.com/docker/docker/api/types/swarm"
|
|
"github.com/docker/docker/errdefs"
|
|
"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 := &Client{
|
|
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
|
|
}
|
|
_, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, types.ServiceCreateOptions{})
|
|
assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
|
|
}
|
|
|
|
// 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{}, types.ServiceCreateOptions{})
|
|
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
|
|
}
|
|
|
|
func TestServiceCreate(t *testing.T) {
|
|
expectedURL := "/services/create"
|
|
client := &Client{
|
|
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
if !strings.HasPrefix(req.URL.Path, expectedURL) {
|
|
return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
|
|
}
|
|
if req.Method != http.MethodPost {
|
|
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
|
|
}
|
|
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
|
|
}),
|
|
}
|
|
|
|
r, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, types.ServiceCreateOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.ID != "service_id" {
|
|
t.Fatalf("expected `service_id`, got %s", r.ID)
|
|
}
|
|
}
|
|
|
|
func TestServiceCreateCompatiblePlatforms(t *testing.T) {
|
|
client := &Client{
|
|
version: "1.30",
|
|
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
if strings.HasPrefix(req.URL.Path, "/v1.30/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, "/v1.30/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)
|
|
}
|
|
}),
|
|
}
|
|
|
|
spec := swarm.ServiceSpec{TaskTemplate: swarm.TaskSpec{ContainerSpec: &swarm.ContainerSpec{Image: "foobar:1.0"}}}
|
|
|
|
r, err := client.ServiceCreate(context.Background(), spec, types.ServiceCreateOptions{QueryRegistry: true})
|
|
assert.Check(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 := &Client{
|
|
version: "1.30",
|
|
client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
if strings.HasPrefix(req.URL.Path, "/v1.30/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, fmt.Errorf("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, "/v1.30/distribution/cannotresolve") {
|
|
// unresolvable image
|
|
return nil, fmt.Errorf("cannot resolve image")
|
|
} else if strings.HasPrefix(req.URL.Path, "/v1.30/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)
|
|
}),
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
},
|
|
}, types.ServiceCreateOptions{QueryRegistry: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if r.ID != "service_id" {
|
|
t.Fatalf("expected `service_id`, got %s", r.ID)
|
|
}
|
|
|
|
if p.expected != serviceCreateImage {
|
|
t.Fatalf("expected image %s, got %s", p.expected, serviceCreateImage)
|
|
}
|
|
}
|
|
}
|