94935f39bc
with a new `proxy` section in the configuration file. Create a new registry type which delegates storage to a proxyBlobStore and proxyManifestStore. These stores will pull through data if not present locally. proxyBlobStore takes care not to write duplicate data to disk. Add a scheduler to cleanup expired content. The scheduler runs as a background goroutine. When a blob or manifest is pulled through from the remote registry, an entry is added to the scheduler with a TTL. When the TTL expires the scheduler calls a pre-specified function to remove the fetched resource. Add token authentication to the registry middleware. Get a token at startup and preload the credential store with the username and password supplied in the config file. Allow resumable digest functionality to be disabled at runtime and disable it when the registry is a pull through cache. Signed-off-by: Richard Scothern <richard.scothern@gmail.com>
313 lines
8.6 KiB
Go
313 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
_ "expvar"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
_ "net/http/pprof"
|
|
"os"
|
|
"time"
|
|
|
|
log "github.com/Sirupsen/logrus"
|
|
"github.com/Sirupsen/logrus/formatters/logstash"
|
|
"github.com/bugsnag/bugsnag-go"
|
|
"github.com/docker/distribution/configuration"
|
|
"github.com/docker/distribution/context"
|
|
_ "github.com/docker/distribution/health"
|
|
_ "github.com/docker/distribution/registry/auth/htpasswd"
|
|
_ "github.com/docker/distribution/registry/auth/silly"
|
|
_ "github.com/docker/distribution/registry/auth/token"
|
|
"github.com/docker/distribution/registry/handlers"
|
|
"github.com/docker/distribution/registry/listener"
|
|
_ "github.com/docker/distribution/registry/proxy"
|
|
_ "github.com/docker/distribution/registry/storage/driver/azure"
|
|
_ "github.com/docker/distribution/registry/storage/driver/filesystem"
|
|
_ "github.com/docker/distribution/registry/storage/driver/inmemory"
|
|
_ "github.com/docker/distribution/registry/storage/driver/middleware/cloudfront"
|
|
_ "github.com/docker/distribution/registry/storage/driver/s3"
|
|
_ "github.com/docker/distribution/registry/storage/driver/swift"
|
|
"github.com/docker/distribution/uuid"
|
|
"github.com/docker/distribution/version"
|
|
gorhandlers "github.com/gorilla/handlers"
|
|
"github.com/yvasiyarov/gorelic"
|
|
)
|
|
|
|
var showVersion bool
|
|
|
|
func init() {
|
|
flag.BoolVar(&showVersion, "version", false, "show the version and exit")
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = usage
|
|
flag.Parse()
|
|
|
|
if showVersion {
|
|
version.PrintVersion()
|
|
return
|
|
}
|
|
|
|
ctx := context.Background()
|
|
ctx = context.WithValue(ctx, "version", version.Version)
|
|
|
|
config, err := resolveConfiguration()
|
|
if err != nil {
|
|
fatalf("configuration error: %v", err)
|
|
}
|
|
|
|
ctx, err = configureLogging(ctx, config)
|
|
if err != nil {
|
|
fatalf("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 = context.GetLogger(ctx).Warnf
|
|
|
|
app := handlers.NewApp(ctx, *config)
|
|
handler := configureReporting(app)
|
|
handler = panicHandler(handler)
|
|
handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler)
|
|
|
|
if config.HTTP.Debug.Addr != "" {
|
|
go debugServer(config.HTTP.Debug.Addr)
|
|
}
|
|
|
|
server := &http.Server{
|
|
Handler: handler,
|
|
}
|
|
|
|
ln, err := listener.NewListener(config.HTTP.Net, config.HTTP.Addr)
|
|
if err != nil {
|
|
context.GetLogger(app).Fatalln(err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
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 {
|
|
context.GetLogger(app).Fatalln(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 {
|
|
context.GetLogger(app).Fatalln(err)
|
|
}
|
|
|
|
if ok := pool.AppendCertsFromPEM(caPem); !ok {
|
|
context.GetLogger(app).Fatalln(fmt.Errorf("Could not add CA to pool"))
|
|
}
|
|
}
|
|
|
|
for _, subj := range pool.Subjects() {
|
|
context.GetLogger(app).Debugf("CA Subject: %s", string(subj))
|
|
}
|
|
|
|
tlsConf.ClientAuth = tls.RequireAndVerifyClientCert
|
|
tlsConf.ClientCAs = pool
|
|
}
|
|
|
|
ln = tls.NewListener(ln, tlsConf)
|
|
context.GetLogger(app).Infof("listening on %v, tls", ln.Addr())
|
|
} else {
|
|
context.GetLogger(app).Infof("listening on %v", ln.Addr())
|
|
}
|
|
|
|
if err := server.Serve(ln); err != nil {
|
|
context.GetLogger(app).Fatalln(err)
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintln(os.Stderr, "usage:", os.Args[0], "<config>")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
func fatalf(format string, args ...interface{}) {
|
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
func resolveConfiguration() (*configuration.Configuration, error) {
|
|
var configurationPath string
|
|
|
|
if flag.NArg() > 0 {
|
|
configurationPath = flag.Arg(0)
|
|
} else if os.Getenv("REGISTRY_CONFIGURATION_PATH") != "" {
|
|
configurationPath = os.Getenv("REGISTRY_CONFIGURATION_PATH")
|
|
}
|
|
|
|
if configurationPath == "" {
|
|
return nil, fmt.Errorf("configuration path unspecified")
|
|
}
|
|
|
|
fp, err := os.Open(configurationPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer fp.Close()
|
|
|
|
config, err := configuration.Parse(fp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing %s: %v", configurationPath, err)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
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 context.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 = context.WithLogger(ctx, context.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 = context.WithLogger(ctx, context.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 = context.WithValues(ctx, config.Log.Fields)
|
|
ctx = context.WithLogger(ctx, context.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
|
|
}
|
|
|
|
// debugServer starts the debug server with pprof, expvar among other
|
|
// endpoints. The addr should not be exposed externally. For most of these to
|
|
// work, tls cannot be enabled on the endpoint, so it is generally separate.
|
|
func debugServer(addr string) {
|
|
log.Infof("debug server listening %v", addr)
|
|
if err := http.ListenAndServe(addr, nil); err != nil {
|
|
log.Fatalf("error listening on debug interface: %v", err)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
})
|
|
}
|