service_create_test.go 6.6 KB

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