gopkg.in/docker/docker.v20@v20.10.27/client/service_create_test.go (about)

     1  package client // import "github.com/docker/docker/client"
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/docker/docker/api/types"
    14  	registrytypes "github.com/docker/docker/api/types/registry"
    15  	"github.com/docker/docker/api/types/swarm"
    16  	"github.com/docker/docker/errdefs"
    17  	digest "github.com/opencontainers/go-digest"
    18  	v1 "github.com/opencontainers/image-spec/specs-go/v1"
    19  	"gotest.tools/v3/assert"
    20  	is "gotest.tools/v3/assert/cmp"
    21  )
    22  
    23  func TestServiceCreateError(t *testing.T) {
    24  	client := &Client{
    25  		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
    26  	}
    27  	_, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, types.ServiceCreateOptions{})
    28  	if !errdefs.IsSystem(err) {
    29  		t.Fatalf("expected a Server Error, got %[1]T: %[1]v", err)
    30  	}
    31  }
    32  
    33  func TestServiceCreate(t *testing.T) {
    34  	expectedURL := "/services/create"
    35  	client := &Client{
    36  		client: newMockClient(func(req *http.Request) (*http.Response, error) {
    37  			if !strings.HasPrefix(req.URL.Path, expectedURL) {
    38  				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
    39  			}
    40  			if req.Method != http.MethodPost {
    41  				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
    42  			}
    43  			b, err := json.Marshal(types.ServiceCreateResponse{
    44  				ID: "service_id",
    45  			})
    46  			if err != nil {
    47  				return nil, err
    48  			}
    49  			return &http.Response{
    50  				StatusCode: http.StatusOK,
    51  				Body:       io.NopCloser(bytes.NewReader(b)),
    52  			}, nil
    53  		}),
    54  	}
    55  
    56  	r, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{}, types.ServiceCreateOptions{})
    57  	if err != nil {
    58  		t.Fatal(err)
    59  	}
    60  	if r.ID != "service_id" {
    61  		t.Fatalf("expected `service_id`, got %s", r.ID)
    62  	}
    63  }
    64  
    65  func TestServiceCreateCompatiblePlatforms(t *testing.T) {
    66  	client := &Client{
    67  		version: "1.30",
    68  		client: newMockClient(func(req *http.Request) (*http.Response, error) {
    69  			if strings.HasPrefix(req.URL.Path, "/v1.30/services/create") {
    70  				var serviceSpec swarm.ServiceSpec
    71  
    72  				// check if the /distribution endpoint returned correct output
    73  				err := json.NewDecoder(req.Body).Decode(&serviceSpec)
    74  				if err != nil {
    75  					return nil, err
    76  				}
    77  
    78  				assert.Check(t, is.Equal("foobar:1.0@sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96", serviceSpec.TaskTemplate.ContainerSpec.Image))
    79  				assert.Check(t, is.Len(serviceSpec.TaskTemplate.Placement.Platforms, 1))
    80  
    81  				p := serviceSpec.TaskTemplate.Placement.Platforms[0]
    82  				b, err := json.Marshal(types.ServiceCreateResponse{
    83  					ID: "service_" + p.OS + "_" + p.Architecture,
    84  				})
    85  				if err != nil {
    86  					return nil, err
    87  				}
    88  				return &http.Response{
    89  					StatusCode: http.StatusOK,
    90  					Body:       io.NopCloser(bytes.NewReader(b)),
    91  				}, nil
    92  			} else if strings.HasPrefix(req.URL.Path, "/v1.30/distribution/") {
    93  				b, err := json.Marshal(registrytypes.DistributionInspect{
    94  					Descriptor: v1.Descriptor{
    95  						Digest: "sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96",
    96  					},
    97  					Platforms: []v1.Platform{
    98  						{
    99  							Architecture: "amd64",
   100  							OS:           "linux",
   101  						},
   102  					},
   103  				})
   104  				if err != nil {
   105  					return nil, err
   106  				}
   107  				return &http.Response{
   108  					StatusCode: http.StatusOK,
   109  					Body:       io.NopCloser(bytes.NewReader(b)),
   110  				}, nil
   111  			} else {
   112  				return nil, fmt.Errorf("unexpected URL '%s'", req.URL.Path)
   113  			}
   114  		}),
   115  	}
   116  
   117  	spec := swarm.ServiceSpec{TaskTemplate: swarm.TaskSpec{ContainerSpec: &swarm.ContainerSpec{Image: "foobar:1.0"}}}
   118  
   119  	r, err := client.ServiceCreate(context.Background(), spec, types.ServiceCreateOptions{QueryRegistry: true})
   120  	assert.Check(t, err)
   121  	assert.Check(t, is.Equal("service_linux_amd64", r.ID))
   122  }
   123  
   124  func TestServiceCreateDigestPinning(t *testing.T) {
   125  	dgst := "sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96"
   126  	dgstAlt := "sha256:37ffbf3f7497c07584dc9637ffbf3f7497c0758c0537ffbf3f7497c0c88e2bb7"
   127  	serviceCreateImage := ""
   128  	pinByDigestTests := []struct {
   129  		img      string // input image provided by the user
   130  		expected string // expected image after digest pinning
   131  	}{
   132  		// default registry returns familiar string
   133  		{"docker.io/library/alpine", "alpine:latest@" + dgst},
   134  		// provided tag is preserved and digest added
   135  		{"alpine:edge", "alpine:edge@" + dgst},
   136  		// image with provided alternative digest remains unchanged
   137  		{"alpine@" + dgstAlt, "alpine@" + dgstAlt},
   138  		// image with provided tag and alternative digest remains unchanged
   139  		{"alpine:edge@" + dgstAlt, "alpine:edge@" + dgstAlt},
   140  		// image on alternative registry does not result in familiar string
   141  		{"alternate.registry/library/alpine", "alternate.registry/library/alpine:latest@" + dgst},
   142  		// unresolvable image does not get a digest
   143  		{"cannotresolve", "cannotresolve:latest"},
   144  	}
   145  
   146  	client := &Client{
   147  		version: "1.30",
   148  		client: newMockClient(func(req *http.Request) (*http.Response, error) {
   149  			if strings.HasPrefix(req.URL.Path, "/v1.30/services/create") {
   150  				// reset and set image received by the service create endpoint
   151  				serviceCreateImage = ""
   152  				var service swarm.ServiceSpec
   153  				if err := json.NewDecoder(req.Body).Decode(&service); err != nil {
   154  					return nil, fmt.Errorf("could not parse service create request")
   155  				}
   156  				serviceCreateImage = service.TaskTemplate.ContainerSpec.Image
   157  
   158  				b, err := json.Marshal(types.ServiceCreateResponse{
   159  					ID: "service_id",
   160  				})
   161  				if err != nil {
   162  					return nil, err
   163  				}
   164  				return &http.Response{
   165  					StatusCode: http.StatusOK,
   166  					Body:       io.NopCloser(bytes.NewReader(b)),
   167  				}, nil
   168  			} else if strings.HasPrefix(req.URL.Path, "/v1.30/distribution/cannotresolve") {
   169  				// unresolvable image
   170  				return nil, fmt.Errorf("cannot resolve image")
   171  			} else if strings.HasPrefix(req.URL.Path, "/v1.30/distribution/") {
   172  				// resolvable images
   173  				b, err := json.Marshal(registrytypes.DistributionInspect{
   174  					Descriptor: v1.Descriptor{
   175  						Digest: digest.Digest(dgst),
   176  					},
   177  				})
   178  				if err != nil {
   179  					return nil, err
   180  				}
   181  				return &http.Response{
   182  					StatusCode: http.StatusOK,
   183  					Body:       io.NopCloser(bytes.NewReader(b)),
   184  				}, nil
   185  			}
   186  			return nil, fmt.Errorf("unexpected URL '%s'", req.URL.Path)
   187  		}),
   188  	}
   189  
   190  	// run pin by digest tests
   191  	for _, p := range pinByDigestTests {
   192  		r, err := client.ServiceCreate(context.Background(), swarm.ServiceSpec{
   193  			TaskTemplate: swarm.TaskSpec{
   194  				ContainerSpec: &swarm.ContainerSpec{
   195  					Image: p.img,
   196  				},
   197  			},
   198  		}, types.ServiceCreateOptions{QueryRegistry: true})
   199  
   200  		if err != nil {
   201  			t.Fatal(err)
   202  		}
   203  
   204  		if r.ID != "service_id" {
   205  			t.Fatalf("expected `service_id`, got %s", r.ID)
   206  		}
   207  
   208  		if p.expected != serviceCreateImage {
   209  			t.Fatalf("expected image %s, got %s", p.expected, serviceCreateImage)
   210  		}
   211  	}
   212  }