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:
parent
8f5f6a4e59
commit
8dd51d6460
6 changed files with 307 additions and 14 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
294
docs/registry.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue