registry.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package registry // import "github.com/docker/docker/testutil/registry"
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "testing"
  10. "time"
  11. "github.com/opencontainers/go-digest"
  12. "gotest.tools/v3/assert"
  13. )
  14. const (
  15. // V2binary is the name of the registry v2 binary
  16. V2binary = "registry-v2"
  17. // V2binarySchema1 is the name of the registry that serve schema1
  18. V2binarySchema1 = "registry-v2-schema1"
  19. // DefaultURL is the default url that will be used by the registry (if not specified otherwise)
  20. DefaultURL = "127.0.0.1:5000"
  21. )
  22. // V2 represent a registry version 2
  23. type V2 struct {
  24. cmd *exec.Cmd
  25. registryURL string
  26. dir string
  27. auth string
  28. username string
  29. password string
  30. email string
  31. }
  32. // Config contains the test registry configuration
  33. type Config struct {
  34. schema1 bool
  35. auth string
  36. tokenURL string
  37. registryURL string
  38. stdout io.Writer
  39. stderr io.Writer
  40. }
  41. // NewV2 creates a v2 registry server
  42. func NewV2(t testing.TB, ops ...func(*Config)) *V2 {
  43. t.Helper()
  44. c := &Config{
  45. registryURL: DefaultURL,
  46. }
  47. for _, op := range ops {
  48. op(c)
  49. }
  50. tmp, err := os.MkdirTemp("", "registry-test-")
  51. assert.NilError(t, err)
  52. template := `version: 0.1
  53. loglevel: debug
  54. storage:
  55. filesystem:
  56. rootdirectory: %s
  57. http:
  58. addr: %s
  59. %s`
  60. var (
  61. authTemplate string
  62. username string
  63. password string
  64. email string
  65. )
  66. switch c.auth {
  67. case "htpasswd":
  68. htpasswdPath := filepath.Join(tmp, "htpasswd")
  69. // generated with: htpasswd -Bbn testuser testpassword
  70. // #nosec G101
  71. userpasswd := "testuser:$2y$05$sBsSqk0OpSD1uTZkHXc4FeJ0Z70wLQdAX/82UiHuQOKbNbBrzs63m"
  72. username = "testuser"
  73. password = "testpassword"
  74. email = "test@test.org"
  75. err := os.WriteFile(htpasswdPath, []byte(userpasswd), os.FileMode(0o644))
  76. assert.NilError(t, err)
  77. authTemplate = fmt.Sprintf(`auth:
  78. htpasswd:
  79. realm: basic-realm
  80. path: %s
  81. `, htpasswdPath)
  82. case "token":
  83. authTemplate = fmt.Sprintf(`auth:
  84. token:
  85. realm: %s
  86. service: "registry"
  87. issuer: "auth-registry"
  88. rootcertbundle: "fixtures/registry/cert.pem"
  89. `, c.tokenURL)
  90. }
  91. confPath := filepath.Join(tmp, "config.yaml")
  92. config, err := os.Create(confPath)
  93. assert.NilError(t, err)
  94. defer config.Close()
  95. if _, err := fmt.Fprintf(config, template, tmp, c.registryURL, authTemplate); err != nil {
  96. // FIXME(vdemeester) use a defer/clean func
  97. os.RemoveAll(tmp)
  98. t.Fatal(err)
  99. }
  100. binary := V2binary
  101. args := []string{"serve", confPath}
  102. if c.schema1 {
  103. binary = V2binarySchema1
  104. args = []string{confPath}
  105. }
  106. cmd := exec.Command(binary, args...)
  107. cmd.Stdout = c.stdout
  108. cmd.Stderr = c.stderr
  109. if err := cmd.Start(); err != nil {
  110. // FIXME(vdemeester) use a defer/clean func
  111. os.RemoveAll(tmp)
  112. t.Fatal(err)
  113. }
  114. return &V2{
  115. cmd: cmd,
  116. dir: tmp,
  117. auth: c.auth,
  118. username: username,
  119. password: password,
  120. email: email,
  121. registryURL: c.registryURL,
  122. }
  123. }
  124. // WaitReady waits for the registry to be ready to serve requests (or fail after a while)
  125. func (r *V2) WaitReady(t testing.TB) {
  126. t.Helper()
  127. var err error
  128. for i := 0; i != 50; i++ {
  129. if err = r.Ping(); err == nil {
  130. return
  131. }
  132. time.Sleep(100 * time.Millisecond)
  133. }
  134. t.Fatalf("timeout waiting for test registry to become available: %v", err)
  135. }
  136. // Ping sends an http request to the current registry, and fail if it doesn't respond correctly
  137. func (r *V2) Ping() error {
  138. // We always ping through HTTP for our test registry.
  139. resp, err := http.Get(fmt.Sprintf("http://%s/v2/", r.registryURL))
  140. if err != nil {
  141. return err
  142. }
  143. resp.Body.Close()
  144. fail := resp.StatusCode != http.StatusOK
  145. if r.auth != "" {
  146. // unauthorized is a _good_ status when pinging v2/ and it needs auth
  147. fail = fail && resp.StatusCode != http.StatusUnauthorized
  148. }
  149. if fail {
  150. return fmt.Errorf("registry ping replied with an unexpected status code %d", resp.StatusCode)
  151. }
  152. return nil
  153. }
  154. // Close kills the registry server
  155. func (r *V2) Close() {
  156. r.cmd.Process.Kill()
  157. r.cmd.Process.Wait()
  158. os.RemoveAll(r.dir)
  159. }
  160. func (r *V2) getBlobFilename(blobDigest digest.Digest) string {
  161. // Split the digest into its algorithm and hex components.
  162. dgstAlg, dgstHex := blobDigest.Algorithm(), blobDigest.Encoded()
  163. // The path to the target blob data looks something like:
  164. // baseDir + "docker/registry/v2/blobs/sha256/a3/a3ed...46d4/data"
  165. return fmt.Sprintf("%s/docker/registry/v2/blobs/%s/%s/%s/data", r.dir, dgstAlg, dgstHex[:2], dgstHex)
  166. }
  167. // ReadBlobContents read the file corresponding to the specified digest
  168. func (r *V2) ReadBlobContents(t testing.TB, blobDigest digest.Digest) []byte {
  169. t.Helper()
  170. // Load the target manifest blob.
  171. manifestBlob, err := os.ReadFile(r.getBlobFilename(blobDigest))
  172. assert.NilError(t, err, "unable to read blob")
  173. return manifestBlob
  174. }
  175. // WriteBlobContents write the file corresponding to the specified digest with the given content
  176. func (r *V2) WriteBlobContents(t testing.TB, blobDigest digest.Digest, data []byte) {
  177. t.Helper()
  178. err := os.WriteFile(r.getBlobFilename(blobDigest), data, os.FileMode(0o644))
  179. assert.NilError(t, err, "unable to write malicious data blob")
  180. }
  181. // TempMoveBlobData moves the existing data file aside, so that we can replace it with a
  182. // malicious blob of data for example.
  183. func (r *V2) TempMoveBlobData(t testing.TB, blobDigest digest.Digest) (undo func()) {
  184. t.Helper()
  185. tempFile, err := os.CreateTemp("", "registry-temp-blob-")
  186. assert.NilError(t, err, "unable to get temporary blob file")
  187. tempFile.Close()
  188. blobFilename := r.getBlobFilename(blobDigest)
  189. // Move the existing data file aside, so that we can replace it with a
  190. // another blob of data.
  191. if err := os.Rename(blobFilename, tempFile.Name()); err != nil {
  192. // FIXME(vdemeester) use a defer/clean func
  193. os.Remove(tempFile.Name())
  194. t.Fatalf("unable to move data blob: %s", err)
  195. }
  196. return func() {
  197. os.Rename(tempFile.Name(), blobFilename)
  198. os.Remove(tempFile.Name())
  199. }
  200. }
  201. // Username returns the configured user name of the server
  202. func (r *V2) Username() string {
  203. return r.username
  204. }
  205. // Password returns the configured password of the server
  206. func (r *V2) Password() string {
  207. return r.password
  208. }
  209. // Email returns the configured email of the server
  210. func (r *V2) Email() string {
  211. return r.email
  212. }
  213. // Path returns the path where the registry write data
  214. func (r *V2) Path() string {
  215. return filepath.Join(r.dir, "docker", "registry", "v2")
  216. }