service_create_test.go 6.7 KB

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