dotcloud/docker

View on GitHub
testutil/registry/registry.go

Summary

Maintainability
A
0 mins
Test Coverage
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")
}