blob: 4d6e1fe5a286b680e6bf5cb0aab5e6872f7b86ef [file] [log] [blame]
// Copyright 2016 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package testenv provides a declarative environment for creating a complete Upspin test tree.
// See testenv_test.go for an example on how to use it.
package testenv
import (
"crypto/rand"
"fmt"
"io/ioutil"
"os"
"strings"
"upspin.io/bind"
"upspin.io/client"
"upspin.io/config"
"upspin.io/errors"
"upspin.io/factotum"
"upspin.io/log"
"upspin.io/test/servermux"
"upspin.io/test/testutil"
"upspin.io/upbox"
"upspin.io/upspin"
"upspin.io/user"
// Implementations that are instantiated explicitly by New.
dirserver_inprocess "upspin.io/dir/inprocess"
dirserver_server "upspin.io/dir/server"
keyserver "upspin.io/key/inprocess"
storeserver "upspin.io/store/inprocess"
// Transports that are selected implicitly by bind.
_ "upspin.io/dir/remote"
_ "upspin.io/key/remote"
_ "upspin.io/store/remote"
)
// The servers that "remote" tests will work against.
const (
TestKeyServer = "key.test.upspin.io:443"
TestStoreServer = "store.test.upspin.io:443"
TestDirServer = "dir.test.upspin.io:443"
TestServerName = "dir-server@upspin.io"
)
// Setup is a configuration structure that contains a directory tree and other optional flags.
type Setup struct {
// OwnerName is the name of the user that runs the tests.
OwnerName upspin.UserName
// Kind is what kind of servers to use, "inprocess", "server", or "remote".
Kind string
// UpBox specifies whether to use upbox to run dirserver,
// storeserver, and keyserver processes separate to the test process.
// If false, the test server instances are run inside the test process.
UpBox bool
// Cache specifies whether to run a cacheserver for the owner.
// This option applies only when UpBox is true.
Cache bool
// Packing is the desired packing for the tree.
Packing upspin.Packing
// Verbose indicates whether we should print verbose debug messages.
Verbose bool
// Cleanup, if present, is run at Exit to clean up any test state necessary.
// It may return an error, which is returned by Exit.
Cleanup func(e *Env) error
}
// Env is the test environment. It contains a client which is the main piece that tests should use.
type Env struct {
// Client is the client tests should use for reaching the newly-created Tree.
Client upspin.Client
// Config is the configuration used when creating the client.
Config upspin.Config
// Setup contains the original setup options.
Setup *Setup
keyServer upspin.KeyServer
storeServer upspin.StoreServer
dirServer upspin.DirServer
schema *upbox.Schema
tmpDir string
exitCalled bool
}
var (
storeServerMux *servermux.Mux
dirServerMux *servermux.Mux
)
func init() {
var store upspin.StoreServer
storeServerMux, store = servermux.NewStore()
bind.RegisterStoreServer(upspin.InProcess, store)
var dir upspin.DirServer
dirServerMux, dir = servermux.NewDir()
bind.RegisterDirServer(upspin.InProcess, dir)
bind.RegisterKeyServer(upspin.InProcess, keyserver.New())
}
func randomEndpoint(prefix string) upspin.Endpoint {
b := make([]byte, 64)
rand.Read(b)
return upspin.Endpoint{
Transport: upspin.InProcess,
NetAddr: upspin.NetAddr(fmt.Sprintf("%s-%x", prefix, b)),
}
}
const upboxYAML = `
users:
- name: %[1]q
- name: %[2]q
cache: %[3]t
servers:
- name: keyserver
user: %[1]q
- name: storeserver
user: %[1]q
- name: dirserver
user: %[1]q
flags:
kind: %[4]s
domain: example.com
`
// New creates a new Env for testing.
func New(setup *Setup) (*Env, error) {
const op errors.Op = "testenv.New"
env := &Env{
Setup: setup,
}
cfg := config.New()
// All tests use the same keyserver, so that users of different
// DirServers can still interact with each other.
cfg = config.SetKeyEndpoint(cfg, upspin.Endpoint{Transport: upspin.InProcess})
switch setup.Kind {
case "inprocess", "server":
if setup.UpBox {
// Use upbox.
yaml := fmt.Sprintf(upboxYAML,
TestServerName,
setup.OwnerName,
setup.Cache,
setup.Kind,
)
schema, err := upbox.SchemaFromYAML(yaml)
if err != nil {
return nil, err
}
if err := schema.Start(); err != nil {
return nil, err
}
env.schema = schema
cfg, err = config.FromFile(schema.Config(string(TestServerName)))
if err != nil {
env.cleanup()
return nil, err
}
env.Config = cfg
break
}
// Test either the dir/inprocess or dir/server implementations
// entire in-memory and offline.
// Set endpoints.
storeEndpoint := randomEndpoint("store")
cfg = config.SetStoreEndpoint(cfg, storeEndpoint)
dirEndpoint := randomEndpoint("dir")
cfg = config.SetDirEndpoint(cfg, dirEndpoint)
// Set up a StoreServer instance. Just use the inprocess
// version for offline tests; the store/server implementation
// isn't interesting when run offline.
env.storeServer = storeserver.New()
storeServerMux.Register(storeEndpoint, env.storeServer)
// Set up user and factotum.
cfg = config.SetUserName(cfg, TestServerName)
f, err := factotum.NewFromDir(testutil.Repo("key", "testdata", TestServerName[:strings.Index(TestServerName, "@")]))
if err != nil {
return nil, errors.E(op, err)
}
cfg = config.SetFactotum(cfg, f)
// Set up DirServer instance.
switch setup.Kind {
case "inprocess":
env.dirServer = dirserver_inprocess.New(cfg)
case "server":
// Create temporary directory for DirServer storage.
logDir, err := ioutil.TempDir("", "testenv-dirserver")
if err != nil {
return nil, errors.E(op, err)
}
env.tmpDir = logDir
env.dirServer, err = dirserver_server.New(cfg, "logDir="+logDir)
if err != nil {
env.cleanup()
return nil, errors.E(op, err)
}
}
dirServerMux.Register(dirEndpoint, env.dirServer)
env.Config = cfg
case "remote":
if setup.UpBox {
return nil, errors.E(op, "UpBox set with incompatible Kind (remote)")
}
cfg = config.SetKeyEndpoint(cfg, upspin.Endpoint{
Transport: upspin.Remote,
NetAddr: TestKeyServer,
})
cfg = config.SetStoreEndpoint(cfg, upspin.Endpoint{
Transport: upspin.Remote,
NetAddr: TestStoreServer,
})
cfg = config.SetDirEndpoint(cfg, upspin.Endpoint{
Transport: upspin.Remote,
NetAddr: TestDirServer,
})
env.Config = cfg
default:
return nil, errors.E(op, errors.Errorf("bad kind %q", setup.Kind))
}
cfg, err := env.NewUser(setup.OwnerName)
if err != nil {
env.cleanup()
return nil, errors.E(op, err)
}
env.Config = cfg
if err := makeRootIfNotExist(env.Config); err != nil {
env.cleanup()
return nil, errors.E(op, err)
}
env.Client = client.New(cfg)
return env, nil
}
// Exit indicates the end of the test environment. It must only be called once. If Setup.Cleanup exists it is called.
func (e *Env) Exit() error {
const op errors.Op = "testenv.Exit"
if e.exitCalled {
return errors.E(op, errors.Invalid, "exit already called")
}
e.exitCalled = true
var firstErr error
check := func(err error) {
if err == nil {
return
}
if firstErr == nil {
firstErr = err
}
log.Debug.Println(op, err)
}
if e.Setup.Cleanup != nil {
check(e.Setup.Cleanup(e))
}
if e.dirServer != nil {
e.dirServer.Close()
}
if e.storeServer != nil {
e.storeServer.Close()
}
if e.keyServer != nil {
e.keyServer.Close()
}
check(e.cleanup())
return firstErr
}
func (e *Env) cleanup() error {
var err error
if e.tmpDir != "" {
err = os.RemoveAll(e.tmpDir)
e.tmpDir = ""
}
if e.schema != nil {
s := e.schema
e.schema = nil
err2 := s.Stop()
if err == nil {
err = err2
}
}
return err
}
// NewUser creates a new client for a user. The new user will not
// have a root created. Callers should use the client to make a root directory if
// necessary.
func (e *Env) NewUser(userName upspin.UserName) (upspin.Config, error) {
const op errors.Op = "testenv.NewUser"
if e.Setup.UpBox {
switch userName {
case e.Setup.OwnerName, TestServerName:
return config.FromFile(e.schema.Config(string(userName)))
}
}
cfg := config.SetUserName(e.Config, userName)
cfg = config.SetCacheEndpoint(cfg, upspin.Endpoint{})
cfg = config.SetPacking(cfg, e.Setup.Packing)
// Set up a factotum for the user.
user, _, _, err := user.Parse(userName)
if err != nil {
return nil, errors.E(op, err)
}
var secrets string
if e.Setup.Kind == "remote" {
secrets = testutil.Repo("key", "testdata", "remote", string(user))
} else {
secrets = testutil.Repo("key", "testdata", string(user))
}
f, err := factotum.NewFromDir(secrets)
if err != nil {
return nil, errors.E(op, userName, err)
}
cfg = config.SetFactotum(cfg, f)
// Don't register users with the test cluster key server;
// our test users should be already registered there.
if e.Setup.Kind != "remote" {
// Register the user with the key server.
err = registerUserWithKeyServer(e.Config, cfg)
if err != nil {
return nil, errors.E(op, err)
}
}
return cfg, nil
}
// registerUserWithKeyServer registers userName's config with the inProcess keyServer.
func registerUserWithKeyServer(server upspin.Config, user upspin.Config) error {
key, err := bind.KeyServer(server, server.KeyEndpoint())
if err != nil {
return err
}
// Install the registered user.
u := &upspin.User{
Name: user.UserName(),
Dirs: []upspin.Endpoint{user.DirEndpoint()},
Stores: []upspin.Endpoint{user.StoreEndpoint()},
PublicKey: user.Factotum().PublicKey(),
}
return key.Put(u)
}
func makeRootIfNotExist(cfg upspin.Config) error {
path := upspin.PathName(cfg.UserName()) + "/"
dir, err := bind.DirServer(cfg, cfg.DirEndpoint())
if err != nil {
return err
}
entry := &upspin.DirEntry{
Name: path,
SignedName: path,
Attr: upspin.AttrDirectory,
}
_, err = dir.Put(entry)
if errors.Is(errors.Exist, err) {
return nil
}
return err
}