package registry // import "github.com/docker/docker/testutil/registry" import ( "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "testing" "time" "github.com/opencontainers/go-digest" "gotest.tools/v3/assert" ) const ( // V2binary is the name of the registry v2 binary V2binary = "registry-v2" // V2binarySchema1 is the name of the registry that serve schema1 V2binarySchema1 = "registry-v2-schema1" // DefaultURL is the default url that will be used by the registry (if not specified otherwise) DefaultURL = "127.0.0.1:5000" ) // V2 represent a registry version 2 type V2 struct { cmd *exec.Cmd registryURL string dir string auth string username string password string email string } // Config contains the test registry configuration type Config struct { schema1 bool auth string tokenURL string registryURL string stdout io.Writer stderr io.Writer } // NewV2 creates a v2 registry server func NewV2(t testing.TB, ops ...func(*Config)) *V2 { t.Helper() c := &Config{ registryURL: DefaultURL, } for _, op := range ops { op(c) } tmp, err := os.MkdirTemp("", "registry-test-") assert.NilError(t, err) template := `version: 0.1 loglevel: debug storage: filesystem: rootdirectory: %s http: addr: %s %s` var ( authTemplate string username string password string email string ) switch c.auth { case "htpasswd": htpasswdPath := filepath.Join(tmp, "htpasswd") // generated with: htpasswd -Bbn testuser testpassword // #nosec G101 userpasswd := "testuser:$2y$05$sBsSqk0OpSD1uTZkHXc4FeJ0Z70wLQdAX/82UiHuQOKbNbBrzs63m" username = "testuser" password = "testpassword" email = "test@test.org" err := os.WriteFile(htpasswdPath, []byte(userpasswd), os.FileMode(0o644)) assert.NilError(t, err) authTemplate = fmt.Sprintf(`auth: htpasswd: realm: basic-realm path: %s `, htpasswdPath) case "token": authTemplate = fmt.Sprintf(`auth: token: realm: %s service: "registry" issuer: "auth-registry" rootcertbundle: "fixtures/registry/cert.pem" `, c.tokenURL) } confPath := filepath.Join(tmp, "config.yaml") config, err := os.Create(confPath) assert.NilError(t, err) defer config.Close() if _, err := fmt.Fprintf(config, template, tmp, c.registryURL, authTemplate); err != nil { // FIXME(vdemeester) use a defer/clean func os.RemoveAll(tmp) t.Fatal(err) } binary := V2binary args := []string{"serve", confPath} if c.schema1 { binary = V2binarySchema1 args = []string{confPath} } cmd := exec.Command(binary, args...) cmd.Stdout = c.stdout cmd.Stderr = c.stderr if err := cmd.Start(); err != nil { // FIXME(vdemeester) use a defer/clean func os.RemoveAll(tmp) t.Fatal(err) } return &V2{ cmd: cmd, dir: tmp, auth: c.auth, username: username, password: password, email: email, registryURL: c.registryURL, } } // WaitReady waits for the registry to be ready to serve requests (or fail after a while) func (r *V2) WaitReady(t testing.TB) { t.Helper() var err error for i := 0; i != 50; i++ { if err = r.Ping(); err == nil { return } time.Sleep(100 * time.Millisecond) } t.Fatalf("timeout waiting for test registry to become available: %v", err) } // Ping sends an http request to the current registry, and fail if it doesn't respond correctly func (r *V2) Ping() error { // We always ping through HTTP for our test registry. resp, err := http.Get(fmt.Sprintf("http://%s/v2/", r.registryURL)) if err != nil { return err } resp.Body.Close() fail := resp.StatusCode != http.StatusOK if r.auth != "" { // unauthorized is a _good_ status when pinging v2/ and it needs auth fail = fail && resp.StatusCode != http.StatusUnauthorized } if fail { return fmt.Errorf("registry ping replied with an unexpected status code %d", resp.StatusCode) } return nil } // Close kills the registry server func (r *V2) Close() { r.cmd.Process.Kill() r.cmd.Process.Wait() os.RemoveAll(r.dir) } func (r *V2) getBlobFilename(blobDigest digest.Digest) string { // Split the digest into its algorithm and hex components. dgstAlg, dgstHex := blobDigest.Algorithm(), blobDigest.Encoded() // The path to the target blob data looks something like: // baseDir + "docker/registry/v2/blobs/sha256/a3/a3ed...46d4/data" return fmt.Sprintf("%s/docker/registry/v2/blobs/%s/%s/%s/data", r.dir, dgstAlg, dgstHex[:2], dgstHex) } // ReadBlobContents read the file corresponding to the specified digest func (r *V2) ReadBlobContents(t testing.TB, blobDigest digest.Digest) []byte { t.Helper() // Load the target manifest blob. manifestBlob, err := os.ReadFile(r.getBlobFilename(blobDigest)) assert.NilError(t, err, "unable to read blob") return manifestBlob } // WriteBlobContents write the file corresponding to the specified digest with the given content func (r *V2) WriteBlobContents(t testing.TB, blobDigest digest.Digest, data []byte) { t.Helper() err := os.WriteFile(r.getBlobFilename(blobDigest), data, os.FileMode(0o644)) assert.NilError(t, err, "unable to write malicious data blob") } // TempMoveBlobData moves the existing data file aside, so that we can replace it with a // malicious blob of data for example. func (r *V2) TempMoveBlobData(t testing.TB, blobDigest digest.Digest) (undo func()) { t.Helper() tempFile, err := os.CreateTemp("", "registry-temp-blob-") assert.NilError(t, err, "unable to get temporary blob file") tempFile.Close() blobFilename := r.getBlobFilename(blobDigest) // Move the existing data file aside, so that we can replace it with a // another blob of data. if err := os.Rename(blobFilename, tempFile.Name()); err != nil { // FIXME(vdemeester) use a defer/clean func os.Remove(tempFile.Name()) t.Fatalf("unable to move data blob: %s", err) } return func() { os.Rename(tempFile.Name(), blobFilename) os.Remove(tempFile.Name()) } } // Username returns the configured user name of the server func (r *V2) Username() string { return r.username } // Password returns the configured password of the server func (r *V2) Password() string { return r.password } // Email returns the configured email of the server func (r *V2) Email() string { return r.email } // Path returns the path where the registry write data func (r *V2) Path() string { return filepath.Join(r.dir, "docker", "registry", "v2") }