service_create_test.go 6.6 KB

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