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 }