Move initialization code from main.go to the registry package

This makes it easier to embed a registry instance inside another
application.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
This commit is contained in:
Aaron Lehmann 2015-08-20 13:56:36 -07:00
parent 8f5f6a4e59
commit 8dd51d6460
6 changed files with 307 additions and 14 deletions

View file

@ -1,3 +1,2 @@
// Package registry is a placeholder package for registry interface // Package registry provides the main entrypoints for running a registry.
// definitions and utilities.
package registry package registry

View file

@ -1038,7 +1038,7 @@ func newTestEnv(t *testing.T, deleteEnabled bool) *testEnv {
func newTestEnvWithConfig(t *testing.T, config *configuration.Configuration) *testEnv { func newTestEnvWithConfig(t *testing.T, config *configuration.Configuration) *testEnv {
ctx := context.Background() ctx := context.Background()
app := NewApp(ctx, *config) app := NewApp(ctx, config)
server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app))
builder, err := v2.NewURLBuilderFromString(server.URL + config.HTTP.Prefix) builder, err := v2.NewURLBuilderFromString(server.URL + config.HTTP.Prefix)

View file

@ -47,7 +47,7 @@ const defaultCheckInterval = 10 * time.Second
type App struct { type App struct {
context.Context context.Context
Config configuration.Configuration Config *configuration.Configuration
router *mux.Router // main application router, configured with dispatchers router *mux.Router // main application router, configured with dispatchers
driver storagedriver.StorageDriver // driver maintains the app global storage driver instance. driver storagedriver.StorageDriver // driver maintains the app global storage driver instance.
@ -69,7 +69,7 @@ type App struct {
// NewApp takes a configuration and returns a configured app, ready to serve // NewApp takes a configuration and returns a configured app, ready to serve
// requests. The app only implements ServeHTTP and can be wrapped in other // requests. The app only implements ServeHTTP and can be wrapped in other
// handlers accordingly. // handlers accordingly.
func NewApp(ctx context.Context, configuration configuration.Configuration) *App { func NewApp(ctx context.Context, configuration *configuration.Configuration) *App {
app := &App{ app := &App{
Config: configuration, Config: configuration,
Context: ctx, Context: ctx,
@ -117,10 +117,10 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App
panic(err) panic(err)
} }
app.configureSecret(&configuration) app.configureSecret(configuration)
app.configureEvents(&configuration) app.configureEvents(configuration)
app.configureRedis(&configuration) app.configureRedis(configuration)
app.configureLogHook(&configuration) app.configureLogHook(configuration)
options := []storage.RegistryOption{} options := []storage.RegistryOption{}

View file

@ -31,7 +31,7 @@ func TestAppDispatcher(t *testing.T) {
t.Fatalf("error creating registry: %v", err) t.Fatalf("error creating registry: %v", err)
} }
app := &App{ app := &App{
Config: configuration.Configuration{}, Config: &configuration.Configuration{},
Context: ctx, Context: ctx,
router: v2.Router(), router: v2.Router(),
driver: driver, driver: driver,
@ -164,7 +164,7 @@ func TestNewApp(t *testing.T) {
// Mostly, with this test, given a sane configuration, we are simply // Mostly, with this test, given a sane configuration, we are simply
// ensuring that NewApp doesn't panic. We might want to tweak this // ensuring that NewApp doesn't panic. We might want to tweak this
// behavior. // behavior.
app := NewApp(ctx, config) app := NewApp(ctx, &config)
server := httptest.NewServer(app) server := httptest.NewServer(app)
builder, err := v2.NewURLBuilderFromString(server.URL) builder, err := v2.NewURLBuilderFromString(server.URL)

View file

@ -23,7 +23,7 @@ func TestFileHealthCheck(t *testing.T) {
} }
defer tmpfile.Close() defer tmpfile.Close()
config := configuration.Configuration{ config := &configuration.Configuration{
Storage: configuration.Storage{ Storage: configuration.Storage{
"inmemory": configuration.Parameters{}, "inmemory": configuration.Parameters{},
}, },
@ -83,7 +83,7 @@ func TestTCPHealthCheck(t *testing.T) {
} }
}() }()
config := configuration.Configuration{ config := &configuration.Configuration{
Storage: configuration.Storage{ Storage: configuration.Storage{
"inmemory": configuration.Parameters{}, "inmemory": configuration.Parameters{},
}, },
@ -142,7 +142,7 @@ func TestHTTPHealthCheck(t *testing.T) {
} }
})) }))
config := configuration.Configuration{ config := &configuration.Configuration{
Storage: configuration.Storage{ Storage: configuration.Storage{
"inmemory": configuration.Parameters{}, "inmemory": configuration.Parameters{},
}, },

294
docs/registry.go Normal file
View file

@ -0,0 +1,294 @@
package registry
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"time"
log "github.com/Sirupsen/logrus"
"github.com/Sirupsen/logrus/formatters/logstash"
"github.com/bugsnag/bugsnag-go"
"github.com/docker/distribution/configuration"
ctxu "github.com/docker/distribution/context"
"github.com/docker/distribution/health"
"github.com/docker/distribution/registry/handlers"
"github.com/docker/distribution/registry/listener"
"github.com/docker/distribution/uuid"
"github.com/docker/distribution/version"
gorhandlers "github.com/gorilla/handlers"
"github.com/yvasiyarov/gorelic"
"golang.org/x/net/context"
)
// A Registry represents a complete instance of the registry.
type Registry struct {
config *configuration.Configuration
app *handlers.App
server *http.Server
ln net.Listener
debugLn net.Listener
}
// NewRegistry creates a new registry from a context and configuration struct.
func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Registry, error) {
// Note this
ctx = ctxu.WithValue(ctx, "version", version.Version)
var err error
ctx, err = configureLogging(ctx, config)
if err != nil {
return nil, fmt.Errorf("error configuring logger: %v", err)
}
// inject a logger into the uuid library. warns us if there is a problem
// with uuid generation under low entropy.
uuid.Loggerf = ctxu.GetLogger(ctx).Warnf
app := handlers.NewApp(ctx, config)
// TODO(aaronl): The global scope of the health checks means NewRegistry
// can only be called once per process.
app.RegisterHealthChecks()
handler := configureReporting(app)
handler = alive("/", handler)
handler = health.Handler(handler)
handler = panicHandler(handler)
handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler)
server := &http.Server{
Handler: handler,
}
ln, err := listener.NewListener(config.HTTP.Net, config.HTTP.Addr)
if err != nil {
return nil, err
}
var debugLn net.Listener
if config.HTTP.Debug.Addr != "" {
debugLn, err = listener.NewListener("tcp", config.HTTP.Debug.Addr)
if err != nil {
return nil, fmt.Errorf("error listening on debug interface: %v", err)
}
log.Infof("debug server listening %v", config.HTTP.Debug.Addr)
}
if config.HTTP.TLS.Certificate != "" {
tlsConf := &tls.Config{
ClientAuth: tls.NoClientCert,
NextProtos: []string{"http/1.1"},
Certificates: make([]tls.Certificate, 1),
MinVersion: tls.VersionTLS10,
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
}
tlsConf.Certificates[0], err = tls.LoadX509KeyPair(config.HTTP.TLS.Certificate, config.HTTP.TLS.Key)
if err != nil {
return nil, err
}
if len(config.HTTP.TLS.ClientCAs) != 0 {
pool := x509.NewCertPool()
for _, ca := range config.HTTP.TLS.ClientCAs {
caPem, err := ioutil.ReadFile(ca)
if err != nil {
return nil, err
}
if ok := pool.AppendCertsFromPEM(caPem); !ok {
return nil, fmt.Errorf("Could not add CA to pool")
}
}
for _, subj := range pool.Subjects() {
ctxu.GetLogger(app).Debugf("CA Subject: %s", string(subj))
}
tlsConf.ClientAuth = tls.RequireAndVerifyClientCert
tlsConf.ClientCAs = pool
}
ln = tls.NewListener(ln, tlsConf)
ctxu.GetLogger(app).Infof("listening on %v, tls", ln.Addr())
} else {
ctxu.GetLogger(app).Infof("listening on %v", ln.Addr())
}
return &Registry{
app: app,
config: config,
server: server,
ln: ln,
debugLn: debugLn,
}, nil
}
// Serve runs the registry's HTTP server(s).
func (registry *Registry) Serve() error {
defer registry.ln.Close()
errChan := make(chan error)
if registry.debugLn != nil {
defer registry.debugLn.Close()
go func() {
errChan <- http.Serve(registry.debugLn, nil)
}()
}
go func() {
errChan <- registry.server.Serve(registry.ln)
}()
return <-errChan
}
func configureReporting(app *handlers.App) http.Handler {
var handler http.Handler = app
if app.Config.Reporting.Bugsnag.APIKey != "" {
bugsnagConfig := bugsnag.Configuration{
APIKey: app.Config.Reporting.Bugsnag.APIKey,
// TODO(brianbland): provide the registry version here
// AppVersion: "2.0",
}
if app.Config.Reporting.Bugsnag.ReleaseStage != "" {
bugsnagConfig.ReleaseStage = app.Config.Reporting.Bugsnag.ReleaseStage
}
if app.Config.Reporting.Bugsnag.Endpoint != "" {
bugsnagConfig.Endpoint = app.Config.Reporting.Bugsnag.Endpoint
}
bugsnag.Configure(bugsnagConfig)
handler = bugsnag.Handler(handler)
}
if app.Config.Reporting.NewRelic.LicenseKey != "" {
agent := gorelic.NewAgent()
agent.NewrelicLicense = app.Config.Reporting.NewRelic.LicenseKey
if app.Config.Reporting.NewRelic.Name != "" {
agent.NewrelicName = app.Config.Reporting.NewRelic.Name
}
agent.CollectHTTPStat = true
agent.Verbose = app.Config.Reporting.NewRelic.Verbose
agent.Run()
handler = agent.WrapHTTPHandler(handler)
}
return handler
}
// configureLogging prepares the context with a logger using the
// configuration.
func configureLogging(ctx ctxu.Context, config *configuration.Configuration) (context.Context, error) {
if config.Log.Level == "" && config.Log.Formatter == "" {
// If no config for logging is set, fallback to deprecated "Loglevel".
log.SetLevel(logLevel(config.Loglevel))
ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, "version"))
return ctx, nil
}
log.SetLevel(logLevel(config.Log.Level))
formatter := config.Log.Formatter
if formatter == "" {
formatter = "text" // default formatter
}
switch formatter {
case "json":
log.SetFormatter(&log.JSONFormatter{
TimestampFormat: time.RFC3339Nano,
})
case "text":
log.SetFormatter(&log.TextFormatter{
TimestampFormat: time.RFC3339Nano,
})
case "logstash":
log.SetFormatter(&logstash.LogstashFormatter{
TimestampFormat: time.RFC3339Nano,
})
default:
// just let the library use default on empty string.
if config.Log.Formatter != "" {
return ctx, fmt.Errorf("unsupported logging formatter: %q", config.Log.Formatter)
}
}
if config.Log.Formatter != "" {
log.Debugf("using %q logging formatter", config.Log.Formatter)
}
// log the application version with messages
ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, "version"))
if len(config.Log.Fields) > 0 {
// build up the static fields, if present.
var fields []interface{}
for k := range config.Log.Fields {
fields = append(fields, k)
}
ctx = ctxu.WithValues(ctx, config.Log.Fields)
ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, fields...))
}
return ctx, nil
}
func logLevel(level configuration.Loglevel) log.Level {
l, err := log.ParseLevel(string(level))
if err != nil {
l = log.InfoLevel
log.Warnf("error parsing level %q: %v, using %q ", level, err, l)
}
return l
}
// panicHandler add a HTTP handler to web app. The handler recover the happening
// panic. logrus.Panic transmits panic message to pre-config log hooks, which is
// defined in config.yml.
func panicHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Panic(fmt.Sprintf("%v", err))
}
}()
handler.ServeHTTP(w, r)
})
}
// alive simply wraps the handler with a route that always returns an http 200
// response when the path is matched. If the path is not matched, the request
// is passed to the provided handler. There is no guarantee of anything but
// that the server is up. Wrap with other handlers (such as health.Handler)
// for greater affect.
func alive(path string, handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == path {
w.Header().Set("Cache-Control", "no-cache")
w.WriteHeader(http.StatusOK)
return
}
handler.ServeHTTP(w, r)
})
}