blob: fbbda7a586e8fa431dff9be6979019f1e85776fc [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.
/*
Command upbox builds and runs Upspin servers as specified by a configuration
file and provides an upspin shell acting as the first user specified by the
configuration.
Configuration files must be in YAML format, of this general form:
users:
- name: joe
- name: jess@example.net
storeserver: store.upspin.io
dirserver: dir.upspin.io
packing: ee
servers:
- name: storeserver
- name: dirserver
user: joe
- name: myserver
importpath: github.com/user/myserver
flags:
debug: cockroach
keyserver: key.uspin.io
domain: exmaple.com
The Users and Servers lists specify the users and servers to create within this
configuration.
Users
Name specifies the user name of this user.
It must be non-empty.
It can be a full email address, or just the user component.
In the latter case, the top-level domain field must be set.
StoreServer and DirServer specify the store and directory endpoints for this
user. If empty, they default to the servers "storeserver" and "dirserver",
respectively. If they are of the form "$servername" then the address of the
server "servername" is used.
Packing specifies the packing method for this user.
If empty, it defaults to "ee".
Servers
Name specifies a short name for this server. It must be non-empty.
The names "keyserver", "storeserver", and "dirserver" represent useful
defaults.
User specifies the user to run this server as.
It can be a full email address, or just the user component.
If empty, the Name of the server is combined with the
Config's Domain and a user is created with that name.
In the latter cases, the top-level Domain field must be set.
ImportPath specifies the import path for this server that is built before
starting the server. If empty, the server Name is appended to the string
"upspin.io/cmd/".
Other top-level fields
KeyServer specifies the KeyServer that each user in the cluster
should use. If it is empty, then a Server named "keyserver" must
be included in the list of Servers, and the address of that server
is used.
Domain specifies a domain that is appended to any user names that do
not include a domain component.
Domain must be specified if any domain suffixes are omitted from
User Names or if a Servers is specified with an empty User field.
Default configuration
If no config is specified, the default configuration is used:
users:
- name: user
servers:
- name: keyserver
- name: storeserver
- name: dirserver
domain: example.com
This creates the users user@example.com, keyserver@example.com,
storeserver@example.com, and dirserver@example.com, builds and runs
the servers keyserver, storeserver, and dirserver (running as their
respective users), and runs "upspin shell" as user@example.com.
*/
package main
import (
"bufio"
"bytes"
"crypto/tls"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
yaml "gopkg.in/yaml.v2"
"upspin.io/upspin"
)
var (
logLevel = flag.String("log", "info", "log `level`")
basePort = flag.Int("port", 8000, "base `port` number for upspin servers")
config = flag.String("config", "", "configuration `file` name")
)
func main() {
flag.Parse()
cfg, err := ConfigFromFile(*config)
if err != nil {
fmt.Fprintln(os.Stderr, "upbox: error parsing config:", err)
os.Exit(1)
}
if err := cfg.Run(); err != nil {
fmt.Fprintln(os.Stderr, "upbox:", err)
os.Exit(1)
}
}
func (cfg *Config) Run() error {
// Build servers and commands.
args := []string{"install", "upspin.io/cmd/upspin"}
for _, s := range cfg.Servers {
args = append(args, s.ImportPath)
}
cmd := exec.Command("go", args...)
cmd.Stdout = prefix("build: ", os.Stdout)
cmd.Stderr = prefix("build: ", os.Stderr)
if err := cmd.Run(); err != nil {
return fmt.Errorf("build error: %v", err)
}
// Create temporary directory.
tmpDir, err := ioutil.TempDir("", "upbox")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
userDir := func(user string) string { return filepath.Join(tmpDir, user) }
// Generate TLS certificates.
if err := generateCert(tmpDir); err != nil {
return err
}
// Generate keys.
// Write an empty file for use by 'upspin keygen'.
configKeygen := filepath.Join(tmpDir, "config.keygen")
if err := ioutil.WriteFile(configKeygen, []byte("secrets: none"), 0644); err != nil {
return err
}
for _, u := range cfg.Users {
fmt.Fprintf(os.Stderr, "upbox: generating keys for user %q\n", u.Name)
dir := userDir(u.Name)
if err := os.MkdirAll(dir, 0700); err != nil {
return err
}
keygen := exec.Command("upspin", "-config="+configKeygen, "keygen", "-where="+dir)
keygen.Stdout = prefix("keygen: ", os.Stdout)
keygen.Stderr = prefix("keygen: ", os.Stderr)
if err := keygen.Run(); err != nil {
return err
}
u.secrets = dir
}
// TODO(adg): make these closures methods on *Config
writeConfig := func(server, user string) (string, error) {
u, ok := cfg.user[user]
if !ok {
return "", fmt.Errorf("unknown user %q", user)
}
configContent := []string{
"username: " + u.Name,
"secrets: " + userDir(user),
"tlscerts: " + tmpDir,
"packing: " + u.Packing,
"storeserver: " + u.StoreServer,
"dirserver: " + u.DirServer,
}
switch server {
case "keyserver":
configContent = append(configContent,
"keyserver: inprocess,",
)
default:
configContent = append(configContent,
"keyserver: remote,"+cfg.KeyServer,
)
}
configFile := filepath.Join(tmpDir, "config."+server)
if err := ioutil.WriteFile(configFile, []byte(strings.Join(configContent, "\n")), 0644); err != nil {
return "", err
}
return configFile, nil
}
startServer := func(s *Server) (*exec.Cmd, error) {
configFile, err := writeConfig(s.Name, s.User)
if err != nil {
return nil, fmt.Errorf("writing config for %v: %v", s.Name, err)
}
args := []string{
"-config=" + configFile,
"-log=" + *logLevel,
"-tls_cert=" + filepath.Join(tmpDir, "cert.pem"),
"-tls_key=" + filepath.Join(tmpDir, "key.pem"),
"-letscache=", // disable
"-https=" + s.addr,
"-addr=" + s.addr,
}
if s.Name == "keyserver" {
args = append(args,
"-test_user="+s.User,
"-test_secrets="+userDir(s.User),
)
}
for k, v := range s.Flags {
args = append(args, fmt.Sprintf("-%s=%v", k, v))
}
cmd := exec.Command(s.Name, args...)
cmd.Stdout = prefix(s.Name+":\t", os.Stdout)
cmd.Stderr = prefix(s.Name+":\t", os.Stderr)
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("starting %v: %v", s.Name, err)
}
return cmd, nil
}
keyUser := cfg.Users[0].Name
if s, ok := cfg.server["keyserver"]; ok {
keyUser = s.User
// Start keyserver.
cmd, err := startServer(s)
if err != nil {
return err
}
defer kill(cmd)
}
// Wait for the keyserver to start and add the users to it.
if err := waitReady(cfg.KeyServer); err != nil {
return err
}
configFile, err := writeConfig("key-bootstrap", keyUser)
if err != nil {
return err
}
for _, u := range cfg.Users {
pk, err := ioutil.ReadFile(filepath.Join(userDir(u.Name), "public.upspinkey"))
if err != nil {
return err
}
dir, err := upspin.ParseEndpoint(u.DirServer)
if err != nil {
return err
}
store, err := upspin.ParseEndpoint(u.StoreServer)
if err != nil {
return err
}
user := &upspin.User{
Name: upspin.UserName(u.Name),
Dirs: []upspin.Endpoint{*dir},
Stores: []upspin.Endpoint{*store},
PublicKey: upspin.PublicKey(pk),
}
userYAML, err := yaml.Marshal(user)
if err != nil {
return err
}
cmd := exec.Command("upspin",
"-config="+configFile,
"-log="+*logLevel,
"user", "-put",
)
cmd.Stdin = bytes.NewReader(userYAML)
cmd.Stdout = prefix("key-bootstrap:\t", os.Stdout)
cmd.Stderr = prefix("key-bootstrap:\t", os.Stderr)
if err := cmd.Run(); err != nil {
return err
}
}
// Start other servers.
for i := range cfg.Servers {
s := cfg.Servers[i]
if s.Name == "keyserver" {
continue
}
cmd, err := startServer(s)
if err != nil {
return err
}
defer kill(cmd)
}
// Wait for the other servers to start.
for _, s := range cfg.Servers {
if s.Name == "keyserver" {
continue
}
if err := waitReady(s.addr); err != nil {
return err
}
}
// Start a shell as the first user.
configFile, err = writeConfig("shell", cfg.Users[0].Name)
if err != nil {
return err
}
args = []string{
"-config=" + configFile,
"-log=" + *logLevel,
"shell",
}
fmt.Fprintf(os.Stderr, "upbox: upspin %s\n", strings.Join(args, " "))
shell := exec.Command("upspin", args...)
shell.Stdin = os.Stdin
shell.Stdout = os.Stdout
shell.Stderr = os.Stderr
return shell.Run()
}
func kill(cmd *exec.Cmd) {
if cmd.Process != nil {
cmd.Process.Kill()
}
}
func prefix(p string, out io.Writer) io.Writer {
r, w := io.Pipe()
go func() {
s := bufio.NewScanner(r)
for s.Scan() {
fmt.Fprintf(out, "%s%s\n", p, s.Bytes())
}
}()
return w
}
func waitReady(addr string) error {
rt := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
req, _ := http.NewRequest("GET", "https://"+addr, nil)
for i := 0; i < 10; i++ {
_, err := rt.RoundTrip(req)
if err != nil {
time.Sleep(time.Second)
continue
}
return nil
}
return fmt.Errorf("timed out waiting for %q to come up", addr)
}