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