moby/testutil/registry/registry.go

242 lines
6.3 KiB
Go
Raw Normal View History

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")
}