|
@@ -13,6 +13,7 @@ import (
|
|
|
"github.com/docker/docker/api/types"
|
|
|
registrytypes "github.com/docker/docker/api/types/registry"
|
|
|
"github.com/docker/docker/api/types/swarm"
|
|
|
+ "github.com/opencontainers/go-digest"
|
|
|
"github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
"golang.org/x/net/context"
|
|
|
)
|
|
@@ -121,3 +122,92 @@ func TestServiceCreateCompatiblePlatforms(t *testing.T) {
|
|
|
t.Fatalf("expected `service_amd64`, got %s", 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{
|
|
|
+ client: newMockClient(func(req *http.Request) (*http.Response, error) {
|
|
|
+ if strings.HasPrefix(req.URL.Path, "/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(types.ServiceCreateResponse{
|
|
|
+ ID: "service_id",
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return &http.Response{
|
|
|
+ StatusCode: http.StatusOK,
|
|
|
+ Body: ioutil.NopCloser(bytes.NewReader(b)),
|
|
|
+ }, nil
|
|
|
+ } else if strings.HasPrefix(req.URL.Path, "/distribution/cannotresolve") {
|
|
|
+ // unresolvable image
|
|
|
+ return nil, fmt.Errorf("cannot resolve image")
|
|
|
+ } else if strings.HasPrefix(req.URL.Path, "/distribution/") {
|
|
|
+ // resolvable images
|
|
|
+ b, err := json.Marshal(registrytypes.DistributionInspect{
|
|
|
+ Descriptor: v1.Descriptor{
|
|
|
+ Digest: digest.Digest(dgst),
|
|
|
+ },
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return &http.Response{
|
|
|
+ StatusCode: http.StatusOK,
|
|
|
+ Body: ioutil.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)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|