mirror of
https://github.com/kubernetes-sigs/prometheus-adapter.git
synced 2026-04-07 02:07:58 +00:00
Add vendor folder to git
This commit is contained in:
parent
66cf5eaafb
commit
183585f56f
6916 changed files with 2629581 additions and 1 deletions
2474
vendor/github.com/docker/distribution/registry/handlers/api_test.go
generated
vendored
Normal file
2474
vendor/github.com/docker/distribution/registry/handlers/api_test.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
998
vendor/github.com/docker/distribution/registry/handlers/app.go
generated
vendored
Normal file
998
vendor/github.com/docker/distribution/registry/handlers/app.go
generated
vendored
Normal file
|
|
@ -0,0 +1,998 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/configuration"
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/health"
|
||||
"github.com/docker/distribution/health/checks"
|
||||
"github.com/docker/distribution/notifications"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/registry/auth"
|
||||
registrymiddleware "github.com/docker/distribution/registry/middleware/registry"
|
||||
repositorymiddleware "github.com/docker/distribution/registry/middleware/repository"
|
||||
"github.com/docker/distribution/registry/proxy"
|
||||
"github.com/docker/distribution/registry/storage"
|
||||
memorycache "github.com/docker/distribution/registry/storage/cache/memory"
|
||||
rediscache "github.com/docker/distribution/registry/storage/cache/redis"
|
||||
storagedriver "github.com/docker/distribution/registry/storage/driver"
|
||||
"github.com/docker/distribution/registry/storage/driver/factory"
|
||||
storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware"
|
||||
"github.com/docker/distribution/version"
|
||||
"github.com/docker/libtrust"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// randomSecretSize is the number of random bytes to generate if no secret
|
||||
// was specified.
|
||||
const randomSecretSize = 32
|
||||
|
||||
// defaultCheckInterval is the default time in between health checks
|
||||
const defaultCheckInterval = 10 * time.Second
|
||||
|
||||
// App is a global registry application object. Shared resources can be placed
|
||||
// on this object that will be accessible from all requests. Any writable
|
||||
// fields should be protected.
|
||||
type App struct {
|
||||
context.Context
|
||||
|
||||
Config *configuration.Configuration
|
||||
|
||||
router *mux.Router // main application router, configured with dispatchers
|
||||
driver storagedriver.StorageDriver // driver maintains the app global storage driver instance.
|
||||
registry distribution.Namespace // registry is the primary registry backend for the app instance.
|
||||
accessController auth.AccessController // main access controller for application
|
||||
|
||||
// httpHost is a parsed representation of the http.host parameter from
|
||||
// the configuration. Only the Scheme and Host fields are used.
|
||||
httpHost url.URL
|
||||
|
||||
// events contains notification related configuration.
|
||||
events struct {
|
||||
sink notifications.Sink
|
||||
source notifications.SourceRecord
|
||||
}
|
||||
|
||||
redis *redis.Pool
|
||||
|
||||
// trustKey is a deprecated key used to sign manifests converted to
|
||||
// schema1 for backward compatibility. It should not be used for any
|
||||
// other purposes.
|
||||
trustKey libtrust.PrivateKey
|
||||
|
||||
// isCache is true if this registry is configured as a pull through cache
|
||||
isCache bool
|
||||
|
||||
// readOnly is true if the registry is in a read-only maintenance mode
|
||||
readOnly bool
|
||||
}
|
||||
|
||||
// NewApp takes a configuration and returns a configured app, ready to serve
|
||||
// requests. The app only implements ServeHTTP and can be wrapped in other
|
||||
// handlers accordingly.
|
||||
func NewApp(ctx context.Context, config *configuration.Configuration) *App {
|
||||
app := &App{
|
||||
Config: config,
|
||||
Context: ctx,
|
||||
router: v2.RouterWithPrefix(config.HTTP.Prefix),
|
||||
isCache: config.Proxy.RemoteURL != "",
|
||||
}
|
||||
|
||||
// Register the handler dispatchers.
|
||||
app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler {
|
||||
return http.HandlerFunc(apiBase)
|
||||
})
|
||||
app.register(v2.RouteNameManifest, imageManifestDispatcher)
|
||||
app.register(v2.RouteNameCatalog, catalogDispatcher)
|
||||
app.register(v2.RouteNameTags, tagsDispatcher)
|
||||
app.register(v2.RouteNameBlob, blobDispatcher)
|
||||
app.register(v2.RouteNameBlobUpload, blobUploadDispatcher)
|
||||
app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher)
|
||||
|
||||
// override the storage driver's UA string for registry outbound HTTP requests
|
||||
storageParams := config.Storage.Parameters()
|
||||
if storageParams == nil {
|
||||
storageParams = make(configuration.Parameters)
|
||||
}
|
||||
storageParams["useragent"] = fmt.Sprintf("docker-distribution/%s %s", version.Version, runtime.Version())
|
||||
|
||||
var err error
|
||||
app.driver, err = factory.Create(config.Storage.Type(), storageParams)
|
||||
if err != nil {
|
||||
// TODO(stevvooe): Move the creation of a service into a protected
|
||||
// method, where this is created lazily. Its status can be queried via
|
||||
// a health check.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
purgeConfig := uploadPurgeDefaultConfig()
|
||||
if mc, ok := config.Storage["maintenance"]; ok {
|
||||
if v, ok := mc["uploadpurging"]; ok {
|
||||
purgeConfig, ok = v.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
panic("uploadpurging config key must contain additional keys")
|
||||
}
|
||||
}
|
||||
if v, ok := mc["readonly"]; ok {
|
||||
readOnly, ok := v.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
panic("readonly config key must contain additional keys")
|
||||
}
|
||||
if readOnlyEnabled, ok := readOnly["enabled"]; ok {
|
||||
app.readOnly, ok = readOnlyEnabled.(bool)
|
||||
if !ok {
|
||||
panic("readonly's enabled config key must have a boolean value")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startUploadPurger(app, app.driver, ctxu.GetLogger(app), purgeConfig)
|
||||
|
||||
app.driver, err = applyStorageMiddleware(app.driver, config.Middleware["storage"])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.configureSecret(config)
|
||||
app.configureEvents(config)
|
||||
app.configureRedis(config)
|
||||
app.configureLogHook(config)
|
||||
|
||||
if config.Compatibility.Schema1.TrustKey != "" {
|
||||
app.trustKey, err = libtrust.LoadKeyFile(config.Compatibility.Schema1.TrustKey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf(`could not load schema1 "signingkey" parameter: %v`, err))
|
||||
}
|
||||
} else {
|
||||
// Generate an ephemeral key to be used for signing converted manifests
|
||||
// for clients that don't support schema2.
|
||||
app.trustKey, err = libtrust.GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if config.HTTP.Host != "" {
|
||||
u, err := url.Parse(config.HTTP.Host)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf(`could not parse http "host" parameter: %v`, err))
|
||||
}
|
||||
app.httpHost = *u
|
||||
}
|
||||
|
||||
options := []storage.RegistryOption{}
|
||||
|
||||
if app.isCache {
|
||||
options = append(options, storage.DisableDigestResumption)
|
||||
}
|
||||
|
||||
if config.Compatibility.Schema1.DisableSignatureStore {
|
||||
options = append(options, storage.DisableSchema1Signatures)
|
||||
options = append(options, storage.Schema1SigningKey(app.trustKey))
|
||||
}
|
||||
|
||||
// configure deletion
|
||||
if d, ok := config.Storage["delete"]; ok {
|
||||
e, ok := d["enabled"]
|
||||
if ok {
|
||||
if deleteEnabled, ok := e.(bool); ok && deleteEnabled {
|
||||
options = append(options, storage.EnableDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// configure redirects
|
||||
var redirectDisabled bool
|
||||
if redirectConfig, ok := config.Storage["redirect"]; ok {
|
||||
v := redirectConfig["disable"]
|
||||
switch v := v.(type) {
|
||||
case bool:
|
||||
redirectDisabled = v
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid type for redirect config: %#v", redirectConfig))
|
||||
}
|
||||
}
|
||||
if redirectDisabled {
|
||||
ctxu.GetLogger(app).Infof("backend redirection disabled")
|
||||
} else {
|
||||
options = append(options, storage.EnableRedirect)
|
||||
}
|
||||
|
||||
// configure storage caches
|
||||
if cc, ok := config.Storage["cache"]; ok {
|
||||
v, ok := cc["blobdescriptor"]
|
||||
if !ok {
|
||||
// Backwards compatible: "layerinfo" == "blobdescriptor"
|
||||
v = cc["layerinfo"]
|
||||
}
|
||||
|
||||
switch v {
|
||||
case "redis":
|
||||
if app.redis == nil {
|
||||
panic("redis configuration required to use for layerinfo cache")
|
||||
}
|
||||
cacheProvider := rediscache.NewRedisBlobDescriptorCacheProvider(app.redis)
|
||||
localOptions := append(options, storage.BlobDescriptorCacheProvider(cacheProvider))
|
||||
app.registry, err = storage.NewRegistry(app, app.driver, localOptions...)
|
||||
if err != nil {
|
||||
panic("could not create registry: " + err.Error())
|
||||
}
|
||||
ctxu.GetLogger(app).Infof("using redis blob descriptor cache")
|
||||
case "inmemory":
|
||||
cacheProvider := memorycache.NewInMemoryBlobDescriptorCacheProvider()
|
||||
localOptions := append(options, storage.BlobDescriptorCacheProvider(cacheProvider))
|
||||
app.registry, err = storage.NewRegistry(app, app.driver, localOptions...)
|
||||
if err != nil {
|
||||
panic("could not create registry: " + err.Error())
|
||||
}
|
||||
ctxu.GetLogger(app).Infof("using inmemory blob descriptor cache")
|
||||
default:
|
||||
if v != "" {
|
||||
ctxu.GetLogger(app).Warnf("unknown cache type %q, caching disabled", config.Storage["cache"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if app.registry == nil {
|
||||
// configure the registry if no cache section is available.
|
||||
app.registry, err = storage.NewRegistry(app.Context, app.driver, options...)
|
||||
if err != nil {
|
||||
panic("could not create registry: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
app.registry, err = applyRegistryMiddleware(app.Context, app.registry, config.Middleware["registry"])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
authType := config.Auth.Type()
|
||||
|
||||
if authType != "" {
|
||||
accessController, err := auth.GetAccessController(config.Auth.Type(), config.Auth.Parameters())
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err))
|
||||
}
|
||||
app.accessController = accessController
|
||||
ctxu.GetLogger(app).Debugf("configured %q access controller", authType)
|
||||
}
|
||||
|
||||
// configure as a pull through cache
|
||||
if config.Proxy.RemoteURL != "" {
|
||||
app.registry, err = proxy.NewRegistryPullThroughCache(ctx, app.registry, app.driver, config.Proxy)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
app.isCache = true
|
||||
ctxu.GetLogger(app).Info("Registry configured as a proxy cache to ", config.Proxy.RemoteURL)
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// RegisterHealthChecks is an awful hack to defer health check registration
|
||||
// control to callers. This should only ever be called once per registry
|
||||
// process, typically in a main function. The correct way would be register
|
||||
// health checks outside of app, since multiple apps may exist in the same
|
||||
// process. Because the configuration and app are tightly coupled,
|
||||
// implementing this properly will require a refactor. This method may panic
|
||||
// if called twice in the same process.
|
||||
func (app *App) RegisterHealthChecks(healthRegistries ...*health.Registry) {
|
||||
if len(healthRegistries) > 1 {
|
||||
panic("RegisterHealthChecks called with more than one registry")
|
||||
}
|
||||
healthRegistry := health.DefaultRegistry
|
||||
if len(healthRegistries) == 1 {
|
||||
healthRegistry = healthRegistries[0]
|
||||
}
|
||||
|
||||
if app.Config.Health.StorageDriver.Enabled {
|
||||
interval := app.Config.Health.StorageDriver.Interval
|
||||
if interval == 0 {
|
||||
interval = defaultCheckInterval
|
||||
}
|
||||
|
||||
storageDriverCheck := func() error {
|
||||
_, err := app.driver.List(app, "/") // "/" should always exist
|
||||
return err // any error will be treated as failure
|
||||
}
|
||||
|
||||
if app.Config.Health.StorageDriver.Threshold != 0 {
|
||||
healthRegistry.RegisterPeriodicThresholdFunc("storagedriver_"+app.Config.Storage.Type(), interval, app.Config.Health.StorageDriver.Threshold, storageDriverCheck)
|
||||
} else {
|
||||
healthRegistry.RegisterPeriodicFunc("storagedriver_"+app.Config.Storage.Type(), interval, storageDriverCheck)
|
||||
}
|
||||
}
|
||||
|
||||
for _, fileChecker := range app.Config.Health.FileCheckers {
|
||||
interval := fileChecker.Interval
|
||||
if interval == 0 {
|
||||
interval = defaultCheckInterval
|
||||
}
|
||||
ctxu.GetLogger(app).Infof("configuring file health check path=%s, interval=%d", fileChecker.File, interval/time.Second)
|
||||
healthRegistry.Register(fileChecker.File, health.PeriodicChecker(checks.FileChecker(fileChecker.File), interval))
|
||||
}
|
||||
|
||||
for _, httpChecker := range app.Config.Health.HTTPCheckers {
|
||||
interval := httpChecker.Interval
|
||||
if interval == 0 {
|
||||
interval = defaultCheckInterval
|
||||
}
|
||||
|
||||
statusCode := httpChecker.StatusCode
|
||||
if statusCode == 0 {
|
||||
statusCode = 200
|
||||
}
|
||||
|
||||
checker := checks.HTTPChecker(httpChecker.URI, statusCode, httpChecker.Timeout, httpChecker.Headers)
|
||||
|
||||
if httpChecker.Threshold != 0 {
|
||||
ctxu.GetLogger(app).Infof("configuring HTTP health check uri=%s, interval=%d, threshold=%d", httpChecker.URI, interval/time.Second, httpChecker.Threshold)
|
||||
healthRegistry.Register(httpChecker.URI, health.PeriodicThresholdChecker(checker, interval, httpChecker.Threshold))
|
||||
} else {
|
||||
ctxu.GetLogger(app).Infof("configuring HTTP health check uri=%s, interval=%d", httpChecker.URI, interval/time.Second)
|
||||
healthRegistry.Register(httpChecker.URI, health.PeriodicChecker(checker, interval))
|
||||
}
|
||||
}
|
||||
|
||||
for _, tcpChecker := range app.Config.Health.TCPCheckers {
|
||||
interval := tcpChecker.Interval
|
||||
if interval == 0 {
|
||||
interval = defaultCheckInterval
|
||||
}
|
||||
|
||||
checker := checks.TCPChecker(tcpChecker.Addr, tcpChecker.Timeout)
|
||||
|
||||
if tcpChecker.Threshold != 0 {
|
||||
ctxu.GetLogger(app).Infof("configuring TCP health check addr=%s, interval=%d, threshold=%d", tcpChecker.Addr, interval/time.Second, tcpChecker.Threshold)
|
||||
healthRegistry.Register(tcpChecker.Addr, health.PeriodicThresholdChecker(checker, interval, tcpChecker.Threshold))
|
||||
} else {
|
||||
ctxu.GetLogger(app).Infof("configuring TCP health check addr=%s, interval=%d", tcpChecker.Addr, interval/time.Second)
|
||||
healthRegistry.Register(tcpChecker.Addr, health.PeriodicChecker(checker, interval))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// register a handler with the application, by route name. The handler will be
|
||||
// passed through the application filters and context will be constructed at
|
||||
// request time.
|
||||
func (app *App) register(routeName string, dispatch dispatchFunc) {
|
||||
|
||||
// TODO(stevvooe): This odd dispatcher/route registration is by-product of
|
||||
// some limitations in the gorilla/mux router. We are using it to keep
|
||||
// routing consistent between the client and server, but we may want to
|
||||
// replace it with manual routing and structure-based dispatch for better
|
||||
// control over the request execution.
|
||||
|
||||
app.router.GetRoute(routeName).Handler(app.dispatcher(dispatch))
|
||||
}
|
||||
|
||||
// configureEvents prepares the event sink for action.
|
||||
func (app *App) configureEvents(configuration *configuration.Configuration) {
|
||||
// Configure all of the endpoint sinks.
|
||||
var sinks []notifications.Sink
|
||||
for _, endpoint := range configuration.Notifications.Endpoints {
|
||||
if endpoint.Disabled {
|
||||
ctxu.GetLogger(app).Infof("endpoint %s disabled, skipping", endpoint.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
ctxu.GetLogger(app).Infof("configuring endpoint %v (%v), timeout=%s, headers=%v", endpoint.Name, endpoint.URL, endpoint.Timeout, endpoint.Headers)
|
||||
endpoint := notifications.NewEndpoint(endpoint.Name, endpoint.URL, notifications.EndpointConfig{
|
||||
Timeout: endpoint.Timeout,
|
||||
Threshold: endpoint.Threshold,
|
||||
Backoff: endpoint.Backoff,
|
||||
Headers: endpoint.Headers,
|
||||
})
|
||||
|
||||
sinks = append(sinks, endpoint)
|
||||
}
|
||||
|
||||
// NOTE(stevvooe): Moving to a new queuing implementation is as easy as
|
||||
// replacing broadcaster with a rabbitmq implementation. It's recommended
|
||||
// that the registry instances also act as the workers to keep deployment
|
||||
// simple.
|
||||
app.events.sink = notifications.NewBroadcaster(sinks...)
|
||||
|
||||
// Populate registry event source
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = configuration.HTTP.Addr
|
||||
} else {
|
||||
// try to pick the port off the config
|
||||
_, port, err := net.SplitHostPort(configuration.HTTP.Addr)
|
||||
if err == nil {
|
||||
hostname = net.JoinHostPort(hostname, port)
|
||||
}
|
||||
}
|
||||
|
||||
app.events.source = notifications.SourceRecord{
|
||||
Addr: hostname,
|
||||
InstanceID: ctxu.GetStringValue(app, "instance.id"),
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) configureRedis(configuration *configuration.Configuration) {
|
||||
if configuration.Redis.Addr == "" {
|
||||
ctxu.GetLogger(app).Infof("redis not configured")
|
||||
return
|
||||
}
|
||||
|
||||
pool := &redis.Pool{
|
||||
Dial: func() (redis.Conn, error) {
|
||||
// TODO(stevvooe): Yet another use case for contextual timing.
|
||||
ctx := context.WithValue(app, "redis.connect.startedat", time.Now())
|
||||
|
||||
done := func(err error) {
|
||||
logger := ctxu.GetLoggerWithField(ctx, "redis.connect.duration",
|
||||
ctxu.Since(ctx, "redis.connect.startedat"))
|
||||
if err != nil {
|
||||
logger.Errorf("redis: error connecting: %v", err)
|
||||
} else {
|
||||
logger.Infof("redis: connect %v", configuration.Redis.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := redis.DialTimeout("tcp",
|
||||
configuration.Redis.Addr,
|
||||
configuration.Redis.DialTimeout,
|
||||
configuration.Redis.ReadTimeout,
|
||||
configuration.Redis.WriteTimeout)
|
||||
if err != nil {
|
||||
ctxu.GetLogger(app).Errorf("error connecting to redis instance %s: %v",
|
||||
configuration.Redis.Addr, err)
|
||||
done(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// authorize the connection
|
||||
if configuration.Redis.Password != "" {
|
||||
if _, err = conn.Do("AUTH", configuration.Redis.Password); err != nil {
|
||||
defer conn.Close()
|
||||
done(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// select the database to use
|
||||
if configuration.Redis.DB != 0 {
|
||||
if _, err = conn.Do("SELECT", configuration.Redis.DB); err != nil {
|
||||
defer conn.Close()
|
||||
done(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
done(nil)
|
||||
return conn, nil
|
||||
},
|
||||
MaxIdle: configuration.Redis.Pool.MaxIdle,
|
||||
MaxActive: configuration.Redis.Pool.MaxActive,
|
||||
IdleTimeout: configuration.Redis.Pool.IdleTimeout,
|
||||
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||
// TODO(stevvooe): We can probably do something more interesting
|
||||
// here with the health package.
|
||||
_, err := c.Do("PING")
|
||||
return err
|
||||
},
|
||||
Wait: false, // if a connection is not avialable, proceed without cache.
|
||||
}
|
||||
|
||||
app.redis = pool
|
||||
|
||||
// setup expvar
|
||||
registry := expvar.Get("registry")
|
||||
if registry == nil {
|
||||
registry = expvar.NewMap("registry")
|
||||
}
|
||||
|
||||
registry.(*expvar.Map).Set("redis", expvar.Func(func() interface{} {
|
||||
return map[string]interface{}{
|
||||
"Config": configuration.Redis,
|
||||
"Active": app.redis.ActiveCount(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// configureLogHook prepares logging hook parameters.
|
||||
func (app *App) configureLogHook(configuration *configuration.Configuration) {
|
||||
entry, ok := ctxu.GetLogger(app).(*log.Entry)
|
||||
if !ok {
|
||||
// somehow, we are not using logrus
|
||||
return
|
||||
}
|
||||
|
||||
logger := entry.Logger
|
||||
|
||||
for _, configHook := range configuration.Log.Hooks {
|
||||
if !configHook.Disabled {
|
||||
switch configHook.Type {
|
||||
case "mail":
|
||||
hook := &logHook{}
|
||||
hook.LevelsParam = configHook.Levels
|
||||
hook.Mail = &mailer{
|
||||
Addr: configHook.MailOptions.SMTP.Addr,
|
||||
Username: configHook.MailOptions.SMTP.Username,
|
||||
Password: configHook.MailOptions.SMTP.Password,
|
||||
Insecure: configHook.MailOptions.SMTP.Insecure,
|
||||
From: configHook.MailOptions.From,
|
||||
To: configHook.MailOptions.To,
|
||||
}
|
||||
logger.Hooks.Add(hook)
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// configureSecret creates a random secret if a secret wasn't included in the
|
||||
// configuration.
|
||||
func (app *App) configureSecret(configuration *configuration.Configuration) {
|
||||
if configuration.HTTP.Secret == "" {
|
||||
var secretBytes [randomSecretSize]byte
|
||||
if _, err := cryptorand.Read(secretBytes[:]); err != nil {
|
||||
panic(fmt.Sprintf("could not generate random bytes for HTTP secret: %v", err))
|
||||
}
|
||||
configuration.HTTP.Secret = string(secretBytes[:])
|
||||
ctxu.GetLogger(app).Warn("No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-balancer. To provide a shared secret, fill in http.secret in the configuration file or set the REGISTRY_HTTP_SECRET environment variable.")
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close() // ensure that request body is always closed.
|
||||
|
||||
// Instantiate an http context here so we can track the error codes
|
||||
// returned by the request router.
|
||||
ctx := defaultContextManager.context(app, w, r)
|
||||
|
||||
defer func() {
|
||||
status, ok := ctx.Value("http.response.status").(int)
|
||||
if ok && status >= 200 && status <= 399 {
|
||||
ctxu.GetResponseLogger(ctx).Infof("response completed")
|
||||
}
|
||||
}()
|
||||
defer defaultContextManager.release(ctx)
|
||||
|
||||
// NOTE(stevvooe): Total hack to get instrumented responsewriter from context.
|
||||
var err error
|
||||
w, err = ctxu.GetResponseWriter(ctx)
|
||||
if err != nil {
|
||||
ctxu.GetLogger(ctx).Warnf("response writer not found in context")
|
||||
}
|
||||
|
||||
// Set a header with the Docker Distribution API Version for all responses.
|
||||
w.Header().Add("Docker-Distribution-API-Version", "registry/2.0")
|
||||
app.router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// dispatchFunc takes a context and request and returns a constructed handler
|
||||
// for the route. The dispatcher will use this to dynamically create request
|
||||
// specific handlers for each endpoint without creating a new router for each
|
||||
// request.
|
||||
type dispatchFunc func(ctx *Context, r *http.Request) http.Handler
|
||||
|
||||
// TODO(stevvooe): dispatchers should probably have some validation error
|
||||
// chain with proper error reporting.
|
||||
|
||||
// dispatcher returns a handler that constructs a request specific context and
|
||||
// handler, using the dispatch factory function.
|
||||
func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
for headerName, headerValues := range app.Config.HTTP.Headers {
|
||||
for _, value := range headerValues {
|
||||
w.Header().Add(headerName, value)
|
||||
}
|
||||
}
|
||||
|
||||
context := app.context(w, r)
|
||||
|
||||
if err := app.authorized(w, r, context); err != nil {
|
||||
ctxu.GetLogger(context).Warnf("error authorizing context: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add username to request logging
|
||||
context.Context = ctxu.WithLogger(context.Context, ctxu.GetLogger(context.Context, auth.UserNameKey))
|
||||
|
||||
if app.nameRequired(r) {
|
||||
nameRef, err := reference.ParseNamed(getName(context))
|
||||
if err != nil {
|
||||
ctxu.GetLogger(context).Errorf("error parsing reference from context: %v", err)
|
||||
context.Errors = append(context.Errors, distribution.ErrRepositoryNameInvalid{
|
||||
Name: getName(context),
|
||||
Reason: err,
|
||||
})
|
||||
if err := errcode.ServeJSON(w, context.Errors); err != nil {
|
||||
ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors)
|
||||
}
|
||||
return
|
||||
}
|
||||
repository, err := app.registry.Repository(context, nameRef)
|
||||
|
||||
if err != nil {
|
||||
ctxu.GetLogger(context).Errorf("error resolving repository: %v", err)
|
||||
|
||||
switch err := err.(type) {
|
||||
case distribution.ErrRepositoryUnknown:
|
||||
context.Errors = append(context.Errors, v2.ErrorCodeNameUnknown.WithDetail(err))
|
||||
case distribution.ErrRepositoryNameInvalid:
|
||||
context.Errors = append(context.Errors, v2.ErrorCodeNameInvalid.WithDetail(err))
|
||||
}
|
||||
|
||||
if err := errcode.ServeJSON(w, context.Errors); err != nil {
|
||||
ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// assign and decorate the authorized repository with an event bridge.
|
||||
context.Repository = notifications.Listen(
|
||||
repository,
|
||||
app.eventBridge(context, r))
|
||||
|
||||
context.Repository, err = applyRepoMiddleware(context.Context, context.Repository, app.Config.Middleware["repository"])
|
||||
if err != nil {
|
||||
ctxu.GetLogger(context).Errorf("error initializing repository middleware: %v", err)
|
||||
context.Errors = append(context.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
|
||||
if err := errcode.ServeJSON(w, context.Errors); err != nil {
|
||||
ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(context, r).ServeHTTP(w, r)
|
||||
// Automated error response handling here. Handlers may return their
|
||||
// own errors if they need different behavior (such as range errors
|
||||
// for layer upload).
|
||||
if context.Errors.Len() > 0 {
|
||||
if err := errcode.ServeJSON(w, context.Errors); err != nil {
|
||||
ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors)
|
||||
}
|
||||
|
||||
app.logError(context, context.Errors)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (app *App) logError(context context.Context, errors errcode.Errors) {
|
||||
for _, e1 := range errors {
|
||||
var c ctxu.Context
|
||||
|
||||
switch e1.(type) {
|
||||
case errcode.Error:
|
||||
e, _ := e1.(errcode.Error)
|
||||
c = ctxu.WithValue(context, "err.code", e.Code)
|
||||
c = ctxu.WithValue(c, "err.message", e.Code.Message())
|
||||
c = ctxu.WithValue(c, "err.detail", e.Detail)
|
||||
case errcode.ErrorCode:
|
||||
e, _ := e1.(errcode.ErrorCode)
|
||||
c = ctxu.WithValue(context, "err.code", e)
|
||||
c = ctxu.WithValue(c, "err.message", e.Message())
|
||||
default:
|
||||
// just normal go 'error'
|
||||
c = ctxu.WithValue(context, "err.code", errcode.ErrorCodeUnknown)
|
||||
c = ctxu.WithValue(c, "err.message", e1.Error())
|
||||
}
|
||||
|
||||
c = ctxu.WithLogger(c, ctxu.GetLogger(c,
|
||||
"err.code",
|
||||
"err.message",
|
||||
"err.detail"))
|
||||
ctxu.GetResponseLogger(c).Errorf("response completed with error")
|
||||
}
|
||||
}
|
||||
|
||||
// context constructs the context object for the application. This only be
|
||||
// called once per request.
|
||||
func (app *App) context(w http.ResponseWriter, r *http.Request) *Context {
|
||||
ctx := defaultContextManager.context(app, w, r)
|
||||
ctx = ctxu.WithVars(ctx, r)
|
||||
ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx,
|
||||
"vars.name",
|
||||
"vars.reference",
|
||||
"vars.digest",
|
||||
"vars.uuid"))
|
||||
|
||||
context := &Context{
|
||||
App: app,
|
||||
Context: ctx,
|
||||
}
|
||||
|
||||
if app.httpHost.Scheme != "" && app.httpHost.Host != "" {
|
||||
// A "host" item in the configuration takes precedence over
|
||||
// X-Forwarded-Proto and X-Forwarded-Host headers, and the
|
||||
// hostname in the request.
|
||||
context.urlBuilder = v2.NewURLBuilder(&app.httpHost, false)
|
||||
} else {
|
||||
context.urlBuilder = v2.NewURLBuilderFromRequest(r, app.Config.HTTP.RelativeURLs)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
// authorized checks if the request can proceed with access to the requested
|
||||
// repository. If it succeeds, the context may access the requested
|
||||
// repository. An error will be returned if access is not available.
|
||||
func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Context) error {
|
||||
ctxu.GetLogger(context).Debug("authorizing request")
|
||||
repo := getName(context)
|
||||
|
||||
if app.accessController == nil {
|
||||
return nil // access controller is not enabled.
|
||||
}
|
||||
|
||||
var accessRecords []auth.Access
|
||||
|
||||
if repo != "" {
|
||||
accessRecords = appendAccessRecords(accessRecords, r.Method, repo)
|
||||
if fromRepo := r.FormValue("from"); fromRepo != "" {
|
||||
// mounting a blob from one repository to another requires pull (GET)
|
||||
// access to the source repository.
|
||||
accessRecords = appendAccessRecords(accessRecords, "GET", fromRepo)
|
||||
}
|
||||
} else {
|
||||
// Only allow the name not to be set on the base route.
|
||||
if app.nameRequired(r) {
|
||||
// For this to be properly secured, repo must always be set for a
|
||||
// resource that may make a modification. The only condition under
|
||||
// which name is not set and we still allow access is when the
|
||||
// base route is accessed. This section prevents us from making
|
||||
// that mistake elsewhere in the code, allowing any operation to
|
||||
// proceed.
|
||||
if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized); err != nil {
|
||||
ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors)
|
||||
}
|
||||
return fmt.Errorf("forbidden: no repository name")
|
||||
}
|
||||
accessRecords = appendCatalogAccessRecord(accessRecords, r)
|
||||
}
|
||||
|
||||
ctx, err := app.accessController.Authorized(context.Context, accessRecords...)
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
case auth.Challenge:
|
||||
// Add the appropriate WWW-Auth header
|
||||
err.SetHeaders(w)
|
||||
|
||||
if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(accessRecords)); err != nil {
|
||||
ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors)
|
||||
}
|
||||
default:
|
||||
// This condition is a potential security problem either in
|
||||
// the configuration or whatever is backing the access
|
||||
// controller. Just return a bad request with no information
|
||||
// to avoid exposure. The request should not proceed.
|
||||
ctxu.GetLogger(context).Errorf("error checking authorization: %v", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(stevvooe): This pattern needs to be cleaned up a bit. One context
|
||||
// should be replaced by another, rather than replacing the context on a
|
||||
// mutable object.
|
||||
context.Context = ctx
|
||||
return nil
|
||||
}
|
||||
|
||||
// eventBridge returns a bridge for the current request, configured with the
|
||||
// correct actor and source.
|
||||
func (app *App) eventBridge(ctx *Context, r *http.Request) notifications.Listener {
|
||||
actor := notifications.ActorRecord{
|
||||
Name: getUserName(ctx, r),
|
||||
}
|
||||
request := notifications.NewRequestRecord(ctxu.GetRequestID(ctx), r)
|
||||
|
||||
return notifications.NewBridge(ctx.urlBuilder, app.events.source, actor, request, app.events.sink)
|
||||
}
|
||||
|
||||
// nameRequired returns true if the route requires a name.
|
||||
func (app *App) nameRequired(r *http.Request) bool {
|
||||
route := mux.CurrentRoute(r)
|
||||
routeName := route.GetName()
|
||||
return route == nil || (routeName != v2.RouteNameBase && routeName != v2.RouteNameCatalog)
|
||||
}
|
||||
|
||||
// apiBase implements a simple yes-man for doing overall checks against the
|
||||
// api. This can support auth roundtrips to support docker login.
|
||||
func apiBase(w http.ResponseWriter, r *http.Request) {
|
||||
const emptyJSON = "{}"
|
||||
// Provide a simple /v2/ 200 OK response with empty json response.
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON)))
|
||||
|
||||
fmt.Fprint(w, emptyJSON)
|
||||
}
|
||||
|
||||
// appendAccessRecords checks the method and adds the appropriate Access records to the records list.
|
||||
func appendAccessRecords(records []auth.Access, method string, repo string) []auth.Access {
|
||||
resource := auth.Resource{
|
||||
Type: "repository",
|
||||
Name: repo,
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "GET", "HEAD":
|
||||
records = append(records,
|
||||
auth.Access{
|
||||
Resource: resource,
|
||||
Action: "pull",
|
||||
})
|
||||
case "POST", "PUT", "PATCH":
|
||||
records = append(records,
|
||||
auth.Access{
|
||||
Resource: resource,
|
||||
Action: "pull",
|
||||
},
|
||||
auth.Access{
|
||||
Resource: resource,
|
||||
Action: "push",
|
||||
})
|
||||
case "DELETE":
|
||||
// DELETE access requires full admin rights, which is represented
|
||||
// as "*". This may not be ideal.
|
||||
records = append(records,
|
||||
auth.Access{
|
||||
Resource: resource,
|
||||
Action: "*",
|
||||
})
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
// Add the access record for the catalog if it's our current route
|
||||
func appendCatalogAccessRecord(accessRecords []auth.Access, r *http.Request) []auth.Access {
|
||||
route := mux.CurrentRoute(r)
|
||||
routeName := route.GetName()
|
||||
|
||||
if routeName == v2.RouteNameCatalog {
|
||||
resource := auth.Resource{
|
||||
Type: "registry",
|
||||
Name: "catalog",
|
||||
}
|
||||
|
||||
accessRecords = append(accessRecords,
|
||||
auth.Access{
|
||||
Resource: resource,
|
||||
Action: "*",
|
||||
})
|
||||
}
|
||||
return accessRecords
|
||||
}
|
||||
|
||||
// applyRegistryMiddleware wraps a registry instance with the configured middlewares
|
||||
func applyRegistryMiddleware(ctx context.Context, registry distribution.Namespace, middlewares []configuration.Middleware) (distribution.Namespace, error) {
|
||||
for _, mw := range middlewares {
|
||||
rmw, err := registrymiddleware.Get(ctx, mw.Name, mw.Options, registry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to configure registry middleware (%s): %s", mw.Name, err)
|
||||
}
|
||||
registry = rmw
|
||||
}
|
||||
return registry, nil
|
||||
|
||||
}
|
||||
|
||||
// applyRepoMiddleware wraps a repository with the configured middlewares
|
||||
func applyRepoMiddleware(ctx context.Context, repository distribution.Repository, middlewares []configuration.Middleware) (distribution.Repository, error) {
|
||||
for _, mw := range middlewares {
|
||||
rmw, err := repositorymiddleware.Get(ctx, mw.Name, mw.Options, repository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repository = rmw
|
||||
}
|
||||
return repository, nil
|
||||
}
|
||||
|
||||
// applyStorageMiddleware wraps a storage driver with the configured middlewares
|
||||
func applyStorageMiddleware(driver storagedriver.StorageDriver, middlewares []configuration.Middleware) (storagedriver.StorageDriver, error) {
|
||||
for _, mw := range middlewares {
|
||||
smw, err := storagemiddleware.Get(mw.Name, mw.Options, driver)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to configure storage middleware (%s): %v", mw.Name, err)
|
||||
}
|
||||
driver = smw
|
||||
}
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
// uploadPurgeDefaultConfig provides a default configuration for upload
|
||||
// purging to be used in the absence of configuration in the
|
||||
// confifuration file
|
||||
func uploadPurgeDefaultConfig() map[interface{}]interface{} {
|
||||
config := map[interface{}]interface{}{}
|
||||
config["enabled"] = true
|
||||
config["age"] = "168h"
|
||||
config["interval"] = "24h"
|
||||
config["dryrun"] = false
|
||||
return config
|
||||
}
|
||||
|
||||
func badPurgeUploadConfig(reason string) {
|
||||
panic(fmt.Sprintf("Unable to parse upload purge configuration: %s", reason))
|
||||
}
|
||||
|
||||
// startUploadPurger schedules a goroutine which will periodically
|
||||
// check upload directories for old files and delete them
|
||||
func startUploadPurger(ctx context.Context, storageDriver storagedriver.StorageDriver, log ctxu.Logger, config map[interface{}]interface{}) {
|
||||
if config["enabled"] == false {
|
||||
return
|
||||
}
|
||||
|
||||
var purgeAgeDuration time.Duration
|
||||
var err error
|
||||
purgeAge, ok := config["age"]
|
||||
if ok {
|
||||
ageStr, ok := purgeAge.(string)
|
||||
if !ok {
|
||||
badPurgeUploadConfig("age is not a string")
|
||||
}
|
||||
purgeAgeDuration, err = time.ParseDuration(ageStr)
|
||||
if err != nil {
|
||||
badPurgeUploadConfig(fmt.Sprintf("Cannot parse duration: %s", err.Error()))
|
||||
}
|
||||
} else {
|
||||
badPurgeUploadConfig("age missing")
|
||||
}
|
||||
|
||||
var intervalDuration time.Duration
|
||||
interval, ok := config["interval"]
|
||||
if ok {
|
||||
intervalStr, ok := interval.(string)
|
||||
if !ok {
|
||||
badPurgeUploadConfig("interval is not a string")
|
||||
}
|
||||
|
||||
intervalDuration, err = time.ParseDuration(intervalStr)
|
||||
if err != nil {
|
||||
badPurgeUploadConfig(fmt.Sprintf("Cannot parse interval: %s", err.Error()))
|
||||
}
|
||||
} else {
|
||||
badPurgeUploadConfig("interval missing")
|
||||
}
|
||||
|
||||
var dryRunBool bool
|
||||
dryRun, ok := config["dryrun"]
|
||||
if ok {
|
||||
dryRunBool, ok = dryRun.(bool)
|
||||
if !ok {
|
||||
badPurgeUploadConfig("cannot parse dryrun")
|
||||
}
|
||||
} else {
|
||||
badPurgeUploadConfig("dryrun missing")
|
||||
}
|
||||
|
||||
go func() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
jitter := time.Duration(rand.Int()%60) * time.Minute
|
||||
log.Infof("Starting upload purge in %s", jitter)
|
||||
time.Sleep(jitter)
|
||||
|
||||
for {
|
||||
storage.PurgeUploads(ctx, storageDriver, time.Now().Add(-purgeAgeDuration), !dryRunBool)
|
||||
log.Infof("Starting upload purge in %s", intervalDuration)
|
||||
time.Sleep(intervalDuration)
|
||||
}
|
||||
}()
|
||||
}
|
||||
274
vendor/github.com/docker/distribution/registry/handlers/app_test.go
generated
vendored
Normal file
274
vendor/github.com/docker/distribution/registry/handlers/app_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/configuration"
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/registry/auth"
|
||||
_ "github.com/docker/distribution/registry/auth/silly"
|
||||
"github.com/docker/distribution/registry/storage"
|
||||
memorycache "github.com/docker/distribution/registry/storage/cache/memory"
|
||||
"github.com/docker/distribution/registry/storage/driver/inmemory"
|
||||
)
|
||||
|
||||
// TestAppDispatcher builds an application with a test dispatcher and ensures
|
||||
// that requests are properly dispatched and the handlers are constructed.
|
||||
// This only tests the dispatch mechanism. The underlying dispatchers must be
|
||||
// tested individually.
|
||||
func TestAppDispatcher(t *testing.T) {
|
||||
driver := inmemory.New()
|
||||
ctx := context.Background()
|
||||
registry, err := storage.NewRegistry(ctx, driver, storage.BlobDescriptorCacheProvider(memorycache.NewInMemoryBlobDescriptorCacheProvider()), storage.EnableDelete, storage.EnableRedirect)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating registry: %v", err)
|
||||
}
|
||||
app := &App{
|
||||
Config: &configuration.Configuration{},
|
||||
Context: ctx,
|
||||
router: v2.Router(),
|
||||
driver: driver,
|
||||
registry: registry,
|
||||
}
|
||||
server := httptest.NewServer(app)
|
||||
router := v2.Router()
|
||||
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing server url: %v", err)
|
||||
}
|
||||
|
||||
varCheckingDispatcher := func(expectedVars map[string]string) dispatchFunc {
|
||||
return func(ctx *Context, r *http.Request) http.Handler {
|
||||
// Always checks the same name context
|
||||
if ctx.Repository.Named().Name() != getName(ctx) {
|
||||
t.Fatalf("unexpected name: %q != %q", ctx.Repository.Named().Name(), "foo/bar")
|
||||
}
|
||||
|
||||
// Check that we have all that is expected
|
||||
for expectedK, expectedV := range expectedVars {
|
||||
if ctx.Value(expectedK) != expectedV {
|
||||
t.Fatalf("unexpected %s in context vars: %q != %q", expectedK, ctx.Value(expectedK), expectedV)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we only have variables that are expected
|
||||
for k, v := range ctx.Value("vars").(map[string]string) {
|
||||
_, ok := expectedVars[k]
|
||||
|
||||
if !ok { // name is checked on context
|
||||
// We have an unexpected key, fail
|
||||
t.Fatalf("unexpected key %q in vars with value %q", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// unflatten a list of variables, suitable for gorilla/mux, to a map[string]string
|
||||
unflatten := func(vars []string) map[string]string {
|
||||
m := make(map[string]string)
|
||||
for i := 0; i < len(vars)-1; i = i + 2 {
|
||||
m[vars[i]] = vars[i+1]
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
for _, testcase := range []struct {
|
||||
endpoint string
|
||||
vars []string
|
||||
}{
|
||||
{
|
||||
endpoint: v2.RouteNameManifest,
|
||||
vars: []string{
|
||||
"name", "foo/bar",
|
||||
"reference", "sometag",
|
||||
},
|
||||
},
|
||||
{
|
||||
endpoint: v2.RouteNameTags,
|
||||
vars: []string{
|
||||
"name", "foo/bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
endpoint: v2.RouteNameBlobUpload,
|
||||
vars: []string{
|
||||
"name", "foo/bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
endpoint: v2.RouteNameBlobUploadChunk,
|
||||
vars: []string{
|
||||
"name", "foo/bar",
|
||||
"uuid", "theuuid",
|
||||
},
|
||||
},
|
||||
} {
|
||||
app.register(testcase.endpoint, varCheckingDispatcher(unflatten(testcase.vars)))
|
||||
route := router.GetRoute(testcase.endpoint).Host(serverURL.Host)
|
||||
u, err := route.URL(testcase.vars...)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, err := http.Get(u.String())
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("unexpected status code: %v != %v", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewApp covers the creation of an application via NewApp with a
|
||||
// configuration.
|
||||
func TestNewApp(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := configuration.Configuration{
|
||||
Storage: configuration.Storage{
|
||||
"inmemory": nil,
|
||||
},
|
||||
Auth: configuration.Auth{
|
||||
// For now, we simply test that new auth results in a viable
|
||||
// application.
|
||||
"silly": {
|
||||
"realm": "realm-test",
|
||||
"service": "service-test",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mostly, with this test, given a sane configuration, we are simply
|
||||
// ensuring that NewApp doesn't panic. We might want to tweak this
|
||||
// behavior.
|
||||
app := NewApp(ctx, &config)
|
||||
|
||||
server := httptest.NewServer(app)
|
||||
builder, err := v2.NewURLBuilderFromString(server.URL, false)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating urlbuilder: %v", err)
|
||||
}
|
||||
|
||||
baseURL, err := builder.BuildBaseURL()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating baseURL: %v", err)
|
||||
}
|
||||
|
||||
// TODO(stevvooe): The rest of this test might belong in the API tests.
|
||||
|
||||
// Just hit the app and make sure we get a 401 Unauthorized error.
|
||||
req, err := http.Get(baseURL)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error during GET: %v", err)
|
||||
}
|
||||
defer req.Body.Close()
|
||||
|
||||
if req.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("unexpected status code during request: %v", err)
|
||||
}
|
||||
|
||||
if req.Header.Get("Content-Type") != "application/json; charset=utf-8" {
|
||||
t.Fatalf("unexpected content-type: %v != %v", req.Header.Get("Content-Type"), "application/json; charset=utf-8")
|
||||
}
|
||||
|
||||
expectedAuthHeader := "Bearer realm=\"realm-test\",service=\"service-test\""
|
||||
if e, a := expectedAuthHeader, req.Header.Get("WWW-Authenticate"); e != a {
|
||||
t.Fatalf("unexpected WWW-Authenticate header: %q != %q", e, a)
|
||||
}
|
||||
|
||||
var errs errcode.Errors
|
||||
dec := json.NewDecoder(req.Body)
|
||||
if err := dec.Decode(&errs); err != nil {
|
||||
t.Fatalf("error decoding error response: %v", err)
|
||||
}
|
||||
|
||||
err2, ok := errs[0].(errcode.ErrorCoder)
|
||||
if !ok {
|
||||
t.Fatalf("not an ErrorCoder: %#v", errs[0])
|
||||
}
|
||||
if err2.ErrorCode() != errcode.ErrorCodeUnauthorized {
|
||||
t.Fatalf("unexpected error code: %v != %v", err2.ErrorCode(), errcode.ErrorCodeUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
// Test the access record accumulator
|
||||
func TestAppendAccessRecords(t *testing.T) {
|
||||
repo := "testRepo"
|
||||
|
||||
expectedResource := auth.Resource{
|
||||
Type: "repository",
|
||||
Name: repo,
|
||||
}
|
||||
|
||||
expectedPullRecord := auth.Access{
|
||||
Resource: expectedResource,
|
||||
Action: "pull",
|
||||
}
|
||||
expectedPushRecord := auth.Access{
|
||||
Resource: expectedResource,
|
||||
Action: "push",
|
||||
}
|
||||
expectedAllRecord := auth.Access{
|
||||
Resource: expectedResource,
|
||||
Action: "*",
|
||||
}
|
||||
|
||||
records := []auth.Access{}
|
||||
result := appendAccessRecords(records, "GET", repo)
|
||||
expectedResult := []auth.Access{expectedPullRecord}
|
||||
if ok := reflect.DeepEqual(result, expectedResult); !ok {
|
||||
t.Fatalf("Actual access record differs from expected")
|
||||
}
|
||||
|
||||
records = []auth.Access{}
|
||||
result = appendAccessRecords(records, "HEAD", repo)
|
||||
expectedResult = []auth.Access{expectedPullRecord}
|
||||
if ok := reflect.DeepEqual(result, expectedResult); !ok {
|
||||
t.Fatalf("Actual access record differs from expected")
|
||||
}
|
||||
|
||||
records = []auth.Access{}
|
||||
result = appendAccessRecords(records, "POST", repo)
|
||||
expectedResult = []auth.Access{expectedPullRecord, expectedPushRecord}
|
||||
if ok := reflect.DeepEqual(result, expectedResult); !ok {
|
||||
t.Fatalf("Actual access record differs from expected")
|
||||
}
|
||||
|
||||
records = []auth.Access{}
|
||||
result = appendAccessRecords(records, "PUT", repo)
|
||||
expectedResult = []auth.Access{expectedPullRecord, expectedPushRecord}
|
||||
if ok := reflect.DeepEqual(result, expectedResult); !ok {
|
||||
t.Fatalf("Actual access record differs from expected")
|
||||
}
|
||||
|
||||
records = []auth.Access{}
|
||||
result = appendAccessRecords(records, "PATCH", repo)
|
||||
expectedResult = []auth.Access{expectedPullRecord, expectedPushRecord}
|
||||
if ok := reflect.DeepEqual(result, expectedResult); !ok {
|
||||
t.Fatalf("Actual access record differs from expected")
|
||||
}
|
||||
|
||||
records = []auth.Access{}
|
||||
result = appendAccessRecords(records, "DELETE", repo)
|
||||
expectedResult = []auth.Access{expectedAllRecord}
|
||||
if ok := reflect.DeepEqual(result, expectedResult); !ok {
|
||||
t.Fatalf("Actual access record differs from expected")
|
||||
}
|
||||
|
||||
}
|
||||
11
vendor/github.com/docker/distribution/registry/handlers/basicauth.go
generated
vendored
Normal file
11
vendor/github.com/docker/distribution/registry/handlers/basicauth.go
generated
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// +build go1.4
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func basicAuth(r *http.Request) (username, password string, ok bool) {
|
||||
return r.BasicAuth()
|
||||
}
|
||||
41
vendor/github.com/docker/distribution/registry/handlers/basicauth_prego14.go
generated
vendored
Normal file
41
vendor/github.com/docker/distribution/registry/handlers/basicauth_prego14.go
generated
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// +build !go1.4
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NOTE(stevvooe): This is basic auth support from go1.4 present to ensure we
|
||||
// can compile on go1.3 and earlier.
|
||||
|
||||
// BasicAuth returns the username and password provided in the request's
|
||||
// Authorization header, if the request uses HTTP Basic Authentication.
|
||||
// See RFC 2617, Section 2.
|
||||
func basicAuth(r *http.Request) (username, password string, ok bool) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return
|
||||
}
|
||||
return parseBasicAuth(auth)
|
||||
}
|
||||
|
||||
// parseBasicAuth parses an HTTP Basic Authentication string.
|
||||
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
|
||||
func parseBasicAuth(auth string) (username, password string, ok bool) {
|
||||
if !strings.HasPrefix(auth, "Basic ") {
|
||||
return
|
||||
}
|
||||
c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic "))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cs := string(c)
|
||||
s := strings.IndexByte(cs, ':')
|
||||
if s < 0 {
|
||||
return
|
||||
}
|
||||
return cs[:s], cs[s+1:], true
|
||||
}
|
||||
99
vendor/github.com/docker/distribution/registry/handlers/blob.go
generated
vendored
Normal file
99
vendor/github.com/docker/distribution/registry/handlers/blob.go
generated
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
// blobDispatcher uses the request context to build a blobHandler.
|
||||
func blobDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||
dgst, err := getDigest(ctx)
|
||||
if err != nil {
|
||||
|
||||
if err == errDigestNotAvailable {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx.Errors = append(ctx.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
|
||||
})
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx.Errors = append(ctx.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
|
||||
})
|
||||
}
|
||||
|
||||
blobHandler := &blobHandler{
|
||||
Context: ctx,
|
||||
Digest: dgst,
|
||||
}
|
||||
|
||||
mhandler := handlers.MethodHandler{
|
||||
"GET": http.HandlerFunc(blobHandler.GetBlob),
|
||||
"HEAD": http.HandlerFunc(blobHandler.GetBlob),
|
||||
}
|
||||
|
||||
if !ctx.readOnly {
|
||||
mhandler["DELETE"] = http.HandlerFunc(blobHandler.DeleteBlob)
|
||||
}
|
||||
|
||||
return mhandler
|
||||
}
|
||||
|
||||
// blobHandler serves http blob requests.
|
||||
type blobHandler struct {
|
||||
*Context
|
||||
|
||||
Digest digest.Digest
|
||||
}
|
||||
|
||||
// GetBlob fetches the binary data from backend storage returns it in the
|
||||
// response.
|
||||
func (bh *blobHandler) GetBlob(w http.ResponseWriter, r *http.Request) {
|
||||
context.GetLogger(bh).Debug("GetBlob")
|
||||
blobs := bh.Repository.Blobs(bh)
|
||||
desc, err := blobs.Stat(bh, bh.Digest)
|
||||
if err != nil {
|
||||
if err == distribution.ErrBlobUnknown {
|
||||
bh.Errors = append(bh.Errors, v2.ErrorCodeBlobUnknown.WithDetail(bh.Digest))
|
||||
} else {
|
||||
bh.Errors = append(bh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := blobs.ServeBlob(bh, w, r, desc.Digest); err != nil {
|
||||
context.GetLogger(bh).Debugf("unexpected error getting blob HTTP handler: %v", err)
|
||||
bh.Errors = append(bh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteBlob deletes a layer blob
|
||||
func (bh *blobHandler) DeleteBlob(w http.ResponseWriter, r *http.Request) {
|
||||
context.GetLogger(bh).Debug("DeleteBlob")
|
||||
|
||||
blobs := bh.Repository.Blobs(bh)
|
||||
err := blobs.Delete(bh, bh.Digest)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case distribution.ErrUnsupported:
|
||||
bh.Errors = append(bh.Errors, errcode.ErrorCodeUnsupported)
|
||||
return
|
||||
case distribution.ErrBlobUnknown:
|
||||
bh.Errors = append(bh.Errors, v2.ErrorCodeBlobUnknown)
|
||||
return
|
||||
default:
|
||||
bh.Errors = append(bh.Errors, err)
|
||||
context.GetLogger(bh).Errorf("Unknown error deleting blob: %s", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Length", "0")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
369
vendor/github.com/docker/distribution/registry/handlers/blobupload.go
generated
vendored
Normal file
369
vendor/github.com/docker/distribution/registry/handlers/blobupload.go
generated
vendored
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/registry/storage"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
// blobUploadDispatcher constructs and returns the blob upload handler for the
|
||||
// given request context.
|
||||
func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||
buh := &blobUploadHandler{
|
||||
Context: ctx,
|
||||
UUID: getUploadUUID(ctx),
|
||||
}
|
||||
|
||||
handler := handlers.MethodHandler{
|
||||
"GET": http.HandlerFunc(buh.GetUploadStatus),
|
||||
"HEAD": http.HandlerFunc(buh.GetUploadStatus),
|
||||
}
|
||||
|
||||
if !ctx.readOnly {
|
||||
handler["POST"] = http.HandlerFunc(buh.StartBlobUpload)
|
||||
handler["PATCH"] = http.HandlerFunc(buh.PatchBlobData)
|
||||
handler["PUT"] = http.HandlerFunc(buh.PutBlobUploadComplete)
|
||||
handler["DELETE"] = http.HandlerFunc(buh.CancelBlobUpload)
|
||||
}
|
||||
|
||||
if buh.UUID != "" {
|
||||
state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state"))
|
||||
if err != nil {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctxu.GetLogger(ctx).Infof("error resolving upload: %v", err)
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
|
||||
})
|
||||
}
|
||||
buh.State = state
|
||||
|
||||
if state.Name != ctx.Repository.Named().Name() {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctxu.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, buh.Repository.Named().Name())
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
|
||||
})
|
||||
}
|
||||
|
||||
if state.UUID != buh.UUID {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctxu.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, buh.UUID)
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
|
||||
})
|
||||
}
|
||||
|
||||
blobs := ctx.Repository.Blobs(buh)
|
||||
upload, err := blobs.Resume(buh, buh.UUID)
|
||||
if err != nil {
|
||||
ctxu.GetLogger(ctx).Errorf("error resolving upload: %v", err)
|
||||
if err == distribution.ErrBlobUploadUnknown {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown.WithDetail(err))
|
||||
})
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
})
|
||||
}
|
||||
buh.Upload = upload
|
||||
|
||||
if size := upload.Size(); size != buh.State.Offset {
|
||||
defer upload.Close()
|
||||
ctxu.GetLogger(ctx).Infof("upload resumed at wrong offest: %d != %d", size, buh.State.Offset)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
|
||||
upload.Cancel(buh)
|
||||
})
|
||||
}
|
||||
return closeResources(handler, buh.Upload)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// blobUploadHandler handles the http blob upload process.
|
||||
type blobUploadHandler struct {
|
||||
*Context
|
||||
|
||||
// UUID identifies the upload instance for the current request. Using UUID
|
||||
// to key blob writers since this implementation uses UUIDs.
|
||||
UUID string
|
||||
|
||||
Upload distribution.BlobWriter
|
||||
|
||||
State blobUploadState
|
||||
}
|
||||
|
||||
// StartBlobUpload begins the blob upload process and allocates a server-side
|
||||
// blob writer session, optionally mounting the blob from a separate repository.
|
||||
func (buh *blobUploadHandler) StartBlobUpload(w http.ResponseWriter, r *http.Request) {
|
||||
var options []distribution.BlobCreateOption
|
||||
|
||||
fromRepo := r.FormValue("from")
|
||||
mountDigest := r.FormValue("mount")
|
||||
|
||||
if mountDigest != "" && fromRepo != "" {
|
||||
opt, err := buh.createBlobMountOption(fromRepo, mountDigest)
|
||||
if opt != nil && err == nil {
|
||||
options = append(options, opt)
|
||||
}
|
||||
}
|
||||
|
||||
blobs := buh.Repository.Blobs(buh)
|
||||
upload, err := blobs.Create(buh, options...)
|
||||
|
||||
if err != nil {
|
||||
if ebm, ok := err.(distribution.ErrBlobMounted); ok {
|
||||
if err := buh.writeBlobCreatedHeaders(w, ebm.Descriptor); err != nil {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
}
|
||||
} else if err == distribution.ErrUnsupported {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported)
|
||||
} else {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
buh.Upload = upload
|
||||
defer buh.Upload.Close()
|
||||
|
||||
if err := buh.blobUploadResponse(w, r, true); err != nil {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Docker-Upload-UUID", buh.Upload.ID())
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// GetUploadStatus returns the status of a given upload, identified by id.
|
||||
func (buh *blobUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if buh.Upload == nil {
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(dmcgowan): Set last argument to false in blobUploadResponse when
|
||||
// resumable upload is supported. This will enable returning a non-zero
|
||||
// range for clients to begin uploading at an offset.
|
||||
if err := buh.blobUploadResponse(w, r, true); err != nil {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Docker-Upload-UUID", buh.UUID)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// PatchBlobData writes data to an upload.
|
||||
func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Request) {
|
||||
if buh.Upload == nil {
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
ct := r.Header.Get("Content-Type")
|
||||
if ct != "" && ct != "application/octet-stream" {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(fmt.Errorf("Bad Content-Type")))
|
||||
// TODO(dmcgowan): encode error
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(dmcgowan): support Content-Range header to seek and write range
|
||||
|
||||
if err := copyFullPayload(w, r, buh.Upload, buh, "blob PATCH", &buh.Errors); err != nil {
|
||||
// copyFullPayload reports the error if necessary
|
||||
return
|
||||
}
|
||||
|
||||
if err := buh.blobUploadResponse(w, r, false); err != nil {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// PutBlobUploadComplete takes the final request of a blob upload. The
|
||||
// request may include all the blob data or no blob data. Any data
|
||||
// provided is received and verified. If successful, the blob is linked
|
||||
// into the blob store and 201 Created is returned with the canonical
|
||||
// url of the blob.
|
||||
func (buh *blobUploadHandler) PutBlobUploadComplete(w http.ResponseWriter, r *http.Request) {
|
||||
if buh.Upload == nil {
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters!
|
||||
|
||||
if dgstStr == "" {
|
||||
// no digest? return error, but allow retry.
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest missing"))
|
||||
return
|
||||
}
|
||||
|
||||
dgst, err := digest.ParseDigest(dgstStr)
|
||||
if err != nil {
|
||||
// no digest? return error, but allow retry.
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest parsing failed"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := copyFullPayload(w, r, buh.Upload, buh, "blob PUT", &buh.Errors); err != nil {
|
||||
// copyFullPayload reports the error if necessary
|
||||
return
|
||||
}
|
||||
|
||||
size := buh.Upload.Size()
|
||||
|
||||
desc, err := buh.Upload.Commit(buh, distribution.Descriptor{
|
||||
Digest: dgst,
|
||||
Size: size,
|
||||
|
||||
// TODO(stevvooe): This isn't wildly important yet, but we should
|
||||
// really set the mediatype. For now, we can let the backend take care
|
||||
// of this.
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
case distribution.ErrBlobInvalidDigest:
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err))
|
||||
default:
|
||||
switch err {
|
||||
case distribution.ErrAccessDenied:
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeDenied)
|
||||
case distribution.ErrUnsupported:
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported)
|
||||
case distribution.ErrBlobInvalidLength, distribution.ErrBlobDigestUnsupported:
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err))
|
||||
default:
|
||||
ctxu.GetLogger(buh).Errorf("unknown error completing upload: %#v", err)
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clean up the backend blob data if there was an error.
|
||||
if err := buh.Upload.Cancel(buh); err != nil {
|
||||
// If the cleanup fails, all we can do is observe and report.
|
||||
ctxu.GetLogger(buh).Errorf("error canceling upload after error: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
if err := buh.writeBlobCreatedHeaders(w, desc); err != nil {
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// CancelBlobUpload cancels an in-progress upload of a blob.
|
||||
func (buh *blobUploadHandler) CancelBlobUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if buh.Upload == nil {
|
||||
buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Docker-Upload-UUID", buh.UUID)
|
||||
if err := buh.Upload.Cancel(buh); err != nil {
|
||||
ctxu.GetLogger(buh).Errorf("error encountered canceling upload: %v", err)
|
||||
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// blobUploadResponse provides a standard request for uploading blobs and
|
||||
// chunk responses. This sets the correct headers but the response status is
|
||||
// left to the caller. The fresh argument is used to ensure that new blob
|
||||
// uploads always start at a 0 offset. This allows disabling resumable push by
|
||||
// always returning a 0 offset on check status.
|
||||
func (buh *blobUploadHandler) blobUploadResponse(w http.ResponseWriter, r *http.Request, fresh bool) error {
|
||||
// TODO(stevvooe): Need a better way to manage the upload state automatically.
|
||||
buh.State.Name = buh.Repository.Named().Name()
|
||||
buh.State.UUID = buh.Upload.ID()
|
||||
buh.State.Offset = buh.Upload.Size()
|
||||
buh.State.StartedAt = buh.Upload.StartedAt()
|
||||
|
||||
token, err := hmacKey(buh.Config.HTTP.Secret).packUploadState(buh.State)
|
||||
if err != nil {
|
||||
ctxu.GetLogger(buh).Infof("error building upload state token: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
uploadURL, err := buh.urlBuilder.BuildBlobUploadChunkURL(
|
||||
buh.Repository.Named(), buh.Upload.ID(),
|
||||
url.Values{
|
||||
"_state": []string{token},
|
||||
})
|
||||
if err != nil {
|
||||
ctxu.GetLogger(buh).Infof("error building upload url: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
endRange := buh.Upload.Size()
|
||||
if endRange > 0 {
|
||||
endRange = endRange - 1
|
||||
}
|
||||
|
||||
w.Header().Set("Docker-Upload-UUID", buh.UUID)
|
||||
w.Header().Set("Location", uploadURL)
|
||||
|
||||
w.Header().Set("Content-Length", "0")
|
||||
w.Header().Set("Range", fmt.Sprintf("0-%d", endRange))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mountBlob attempts to mount a blob from another repository by its digest. If
|
||||
// successful, the blob is linked into the blob store and 201 Created is
|
||||
// returned with the canonical url of the blob.
|
||||
func (buh *blobUploadHandler) createBlobMountOption(fromRepo, mountDigest string) (distribution.BlobCreateOption, error) {
|
||||
dgst, err := digest.ParseDigest(mountDigest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref, err := reference.ParseNamed(fromRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canonical, err := reference.WithDigest(ref, dgst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storage.WithMountFrom(canonical), nil
|
||||
}
|
||||
|
||||
// writeBlobCreatedHeaders writes the standard headers describing a newly
|
||||
// created blob. A 201 Created is written as well as the canonical URL and
|
||||
// blob digest.
|
||||
func (buh *blobUploadHandler) writeBlobCreatedHeaders(w http.ResponseWriter, desc distribution.Descriptor) error {
|
||||
ref, err := reference.WithDigest(buh.Repository.Named(), desc.Digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobURL, err := buh.urlBuilder.BuildBlobURL(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Location", blobURL)
|
||||
w.Header().Set("Content-Length", "0")
|
||||
w.Header().Set("Docker-Content-Digest", desc.Digest.String())
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
return nil
|
||||
}
|
||||
95
vendor/github.com/docker/distribution/registry/handlers/catalog.go
generated
vendored
Normal file
95
vendor/github.com/docker/distribution/registry/handlers/catalog.go
generated
vendored
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
const maximumReturnedEntries = 100
|
||||
|
||||
func catalogDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||
catalogHandler := &catalogHandler{
|
||||
Context: ctx,
|
||||
}
|
||||
|
||||
return handlers.MethodHandler{
|
||||
"GET": http.HandlerFunc(catalogHandler.GetCatalog),
|
||||
}
|
||||
}
|
||||
|
||||
type catalogHandler struct {
|
||||
*Context
|
||||
}
|
||||
|
||||
type catalogAPIResponse struct {
|
||||
Repositories []string `json:"repositories"`
|
||||
}
|
||||
|
||||
func (ch *catalogHandler) GetCatalog(w http.ResponseWriter, r *http.Request) {
|
||||
var moreEntries = true
|
||||
|
||||
q := r.URL.Query()
|
||||
lastEntry := q.Get("last")
|
||||
maxEntries, err := strconv.Atoi(q.Get("n"))
|
||||
if err != nil || maxEntries < 0 {
|
||||
maxEntries = maximumReturnedEntries
|
||||
}
|
||||
|
||||
repos := make([]string, maxEntries)
|
||||
|
||||
filled, err := ch.App.registry.Repositories(ch.Context, repos, lastEntry)
|
||||
if err == io.EOF {
|
||||
moreEntries = false
|
||||
} else if err != nil {
|
||||
ch.Errors = append(ch.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
// Add a link header if there are more entries to retrieve
|
||||
if moreEntries {
|
||||
lastEntry = repos[len(repos)-1]
|
||||
urlStr, err := createLinkEntry(r.URL.String(), maxEntries, lastEntry)
|
||||
if err != nil {
|
||||
ch.Errors = append(ch.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Link", urlStr)
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
if err := enc.Encode(catalogAPIResponse{
|
||||
Repositories: repos[0:filled],
|
||||
}); err != nil {
|
||||
ch.Errors = append(ch.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Use the original URL from the request to create a new URL for
|
||||
// the link header
|
||||
func createLinkEntry(origURL string, maxEntries int, lastEntry string) (string, error) {
|
||||
calledURL, err := url.Parse(origURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Add("n", strconv.Itoa(maxEntries))
|
||||
v.Add("last", lastEntry)
|
||||
|
||||
calledURL.RawQuery = v.Encode()
|
||||
|
||||
calledURL.Fragment = ""
|
||||
urlStr := fmt.Sprintf("<%s>; rel=\"next\"", calledURL.String())
|
||||
|
||||
return urlStr, nil
|
||||
}
|
||||
152
vendor/github.com/docker/distribution/registry/handlers/context.go
generated
vendored
Normal file
152
vendor/github.com/docker/distribution/registry/handlers/context.go
generated
vendored
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/registry/auth"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Context should contain the request specific context for use in across
|
||||
// handlers. Resources that don't need to be shared across handlers should not
|
||||
// be on this object.
|
||||
type Context struct {
|
||||
// App points to the application structure that created this context.
|
||||
*App
|
||||
context.Context
|
||||
|
||||
// Repository is the repository for the current request. All requests
|
||||
// should be scoped to a single repository. This field may be nil.
|
||||
Repository distribution.Repository
|
||||
|
||||
// Errors is a collection of errors encountered during the request to be
|
||||
// returned to the client API. If errors are added to the collection, the
|
||||
// handler *must not* start the response via http.ResponseWriter.
|
||||
Errors errcode.Errors
|
||||
|
||||
urlBuilder *v2.URLBuilder
|
||||
|
||||
// TODO(stevvooe): The goal is too completely factor this context and
|
||||
// dispatching out of the web application. Ideally, we should lean on
|
||||
// context.Context for injection of these resources.
|
||||
}
|
||||
|
||||
// Value overrides context.Context.Value to ensure that calls are routed to
|
||||
// correct context.
|
||||
func (ctx *Context) Value(key interface{}) interface{} {
|
||||
return ctx.Context.Value(key)
|
||||
}
|
||||
|
||||
func getName(ctx context.Context) (name string) {
|
||||
return ctxu.GetStringValue(ctx, "vars.name")
|
||||
}
|
||||
|
||||
func getReference(ctx context.Context) (reference string) {
|
||||
return ctxu.GetStringValue(ctx, "vars.reference")
|
||||
}
|
||||
|
||||
var errDigestNotAvailable = fmt.Errorf("digest not available in context")
|
||||
|
||||
func getDigest(ctx context.Context) (dgst digest.Digest, err error) {
|
||||
dgstStr := ctxu.GetStringValue(ctx, "vars.digest")
|
||||
|
||||
if dgstStr == "" {
|
||||
ctxu.GetLogger(ctx).Errorf("digest not available")
|
||||
return "", errDigestNotAvailable
|
||||
}
|
||||
|
||||
d, err := digest.ParseDigest(dgstStr)
|
||||
if err != nil {
|
||||
ctxu.GetLogger(ctx).Errorf("error parsing digest=%q: %v", dgstStr, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func getUploadUUID(ctx context.Context) (uuid string) {
|
||||
return ctxu.GetStringValue(ctx, "vars.uuid")
|
||||
}
|
||||
|
||||
// getUserName attempts to resolve a username from the context and request. If
|
||||
// a username cannot be resolved, the empty string is returned.
|
||||
func getUserName(ctx context.Context, r *http.Request) string {
|
||||
username := ctxu.GetStringValue(ctx, auth.UserNameKey)
|
||||
|
||||
// Fallback to request user with basic auth
|
||||
if username == "" {
|
||||
var ok bool
|
||||
uname, _, ok := basicAuth(r)
|
||||
if ok {
|
||||
username = uname
|
||||
}
|
||||
}
|
||||
|
||||
return username
|
||||
}
|
||||
|
||||
// contextManager allows us to associate net/context.Context instances with a
|
||||
// request, based on the memory identity of http.Request. This prepares http-
|
||||
// level context, which is not application specific. If this is called,
|
||||
// (*contextManager).release must be called on the context when the request is
|
||||
// completed.
|
||||
//
|
||||
// Providing this circumvents a lot of necessity for dispatchers with the
|
||||
// benefit of instantiating the request context much earlier.
|
||||
//
|
||||
// TODO(stevvooe): Consider making this facility a part of the context package.
|
||||
type contextManager struct {
|
||||
contexts map[*http.Request]context.Context
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// defaultContextManager is just a global instance to register request contexts.
|
||||
var defaultContextManager = newContextManager()
|
||||
|
||||
func newContextManager() *contextManager {
|
||||
return &contextManager{
|
||||
contexts: make(map[*http.Request]context.Context),
|
||||
}
|
||||
}
|
||||
|
||||
// context either returns a new context or looks it up in the manager.
|
||||
func (cm *contextManager) context(parent context.Context, w http.ResponseWriter, r *http.Request) context.Context {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
ctx, ok := cm.contexts[r]
|
||||
if ok {
|
||||
return ctx
|
||||
}
|
||||
|
||||
if parent == nil {
|
||||
parent = ctxu.Background()
|
||||
}
|
||||
|
||||
ctx = ctxu.WithRequest(parent, r)
|
||||
ctx, w = ctxu.WithResponseWriter(ctx, w)
|
||||
ctx = ctxu.WithLogger(ctx, ctxu.GetRequestLogger(ctx))
|
||||
cm.contexts[r] = ctx
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// releases frees any associated with resources from request.
|
||||
func (cm *contextManager) release(ctx context.Context) {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
r, err := ctxu.GetRequest(ctx)
|
||||
if err != nil {
|
||||
ctxu.GetLogger(ctx).Errorf("no request found in context during release")
|
||||
return
|
||||
}
|
||||
delete(cm.contexts, r)
|
||||
}
|
||||
201
vendor/github.com/docker/distribution/registry/handlers/health_test.go
generated
vendored
Normal file
201
vendor/github.com/docker/distribution/registry/handlers/health_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/configuration"
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/health"
|
||||
)
|
||||
|
||||
func TestFileHealthCheck(t *testing.T) {
|
||||
interval := time.Second
|
||||
|
||||
tmpfile, err := ioutil.TempFile(os.TempDir(), "healthcheck")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create temporary file: %v", err)
|
||||
}
|
||||
defer tmpfile.Close()
|
||||
|
||||
config := &configuration.Configuration{
|
||||
Storage: configuration.Storage{
|
||||
"inmemory": configuration.Parameters{},
|
||||
},
|
||||
Health: configuration.Health{
|
||||
FileCheckers: []configuration.FileChecker{
|
||||
{
|
||||
Interval: interval,
|
||||
File: tmpfile.Name(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
app := NewApp(ctx, config)
|
||||
healthRegistry := health.NewRegistry()
|
||||
app.RegisterHealthChecks(healthRegistry)
|
||||
|
||||
// Wait for health check to happen
|
||||
<-time.After(2 * interval)
|
||||
|
||||
status := healthRegistry.CheckStatus()
|
||||
if len(status) != 1 {
|
||||
t.Fatal("expected 1 item in health check results")
|
||||
}
|
||||
if status[tmpfile.Name()] != "file exists" {
|
||||
t.Fatal(`did not get "file exists" result for health check`)
|
||||
}
|
||||
|
||||
os.Remove(tmpfile.Name())
|
||||
|
||||
<-time.After(2 * interval)
|
||||
if len(healthRegistry.CheckStatus()) != 0 {
|
||||
t.Fatal("expected 0 items in health check results")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPHealthCheck(t *testing.T) {
|
||||
interval := time.Second
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create listener: %v", err)
|
||||
}
|
||||
addrStr := ln.Addr().String()
|
||||
|
||||
// Start accepting
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
// listener was closed
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
config := &configuration.Configuration{
|
||||
Storage: configuration.Storage{
|
||||
"inmemory": configuration.Parameters{},
|
||||
},
|
||||
Health: configuration.Health{
|
||||
TCPCheckers: []configuration.TCPChecker{
|
||||
{
|
||||
Interval: interval,
|
||||
Addr: addrStr,
|
||||
Timeout: 500 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
app := NewApp(ctx, config)
|
||||
healthRegistry := health.NewRegistry()
|
||||
app.RegisterHealthChecks(healthRegistry)
|
||||
|
||||
// Wait for health check to happen
|
||||
<-time.After(2 * interval)
|
||||
|
||||
if len(healthRegistry.CheckStatus()) != 0 {
|
||||
t.Fatal("expected 0 items in health check results")
|
||||
}
|
||||
|
||||
ln.Close()
|
||||
<-time.After(2 * interval)
|
||||
|
||||
// Health check should now fail
|
||||
status := healthRegistry.CheckStatus()
|
||||
if len(status) != 1 {
|
||||
t.Fatal("expected 1 item in health check results")
|
||||
}
|
||||
if status[addrStr] != "connection to "+addrStr+" failed" {
|
||||
t.Fatal(`did not get "connection failed" result for health check`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPHealthCheck(t *testing.T) {
|
||||
interval := time.Second
|
||||
threshold := 3
|
||||
|
||||
stopFailing := make(chan struct{})
|
||||
|
||||
checkedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "HEAD" {
|
||||
t.Fatalf("expected HEAD request, got %s", r.Method)
|
||||
}
|
||||
select {
|
||||
case <-stopFailing:
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
|
||||
config := &configuration.Configuration{
|
||||
Storage: configuration.Storage{
|
||||
"inmemory": configuration.Parameters{},
|
||||
},
|
||||
Health: configuration.Health{
|
||||
HTTPCheckers: []configuration.HTTPChecker{
|
||||
{
|
||||
Interval: interval,
|
||||
URI: checkedServer.URL,
|
||||
Threshold: threshold,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
app := NewApp(ctx, config)
|
||||
healthRegistry := health.NewRegistry()
|
||||
app.RegisterHealthChecks(healthRegistry)
|
||||
|
||||
for i := 0; ; i++ {
|
||||
<-time.After(interval)
|
||||
|
||||
status := healthRegistry.CheckStatus()
|
||||
|
||||
if i < threshold-1 {
|
||||
// definitely shouldn't have hit the threshold yet
|
||||
if len(status) != 0 {
|
||||
t.Fatal("expected 1 item in health check results")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if i < threshold+1 {
|
||||
// right on the threshold - don't expect a failure yet
|
||||
continue
|
||||
}
|
||||
|
||||
if len(status) != 1 {
|
||||
t.Fatal("expected 1 item in health check results")
|
||||
}
|
||||
if status[checkedServer.URL] != "downstream service returned unexpected status: 500" {
|
||||
t.Fatal("did not get expected result for health check")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
// Signal HTTP handler to start returning 200
|
||||
close(stopFailing)
|
||||
|
||||
<-time.After(2 * interval)
|
||||
|
||||
if len(healthRegistry.CheckStatus()) != 0 {
|
||||
t.Fatal("expected 0 items in health check results")
|
||||
}
|
||||
}
|
||||
66
vendor/github.com/docker/distribution/registry/handlers/helpers.go
generated
vendored
Normal file
66
vendor/github.com/docker/distribution/registry/handlers/helpers.go
generated
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
)
|
||||
|
||||
// closeResources closes all the provided resources after running the target
|
||||
// handler.
|
||||
func closeResources(handler http.Handler, closers ...io.Closer) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
for _, closer := range closers {
|
||||
defer closer.Close()
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// copyFullPayload copies the payload of a HTTP request to destWriter. If it
|
||||
// receives less content than expected, and the client disconnected during the
|
||||
// upload, it avoids sending a 400 error to keep the logs cleaner.
|
||||
func copyFullPayload(responseWriter http.ResponseWriter, r *http.Request, destWriter io.Writer, context ctxu.Context, action string, errSlice *errcode.Errors) error {
|
||||
// Get a channel that tells us if the client disconnects
|
||||
var clientClosed <-chan bool
|
||||
if notifier, ok := responseWriter.(http.CloseNotifier); ok {
|
||||
clientClosed = notifier.CloseNotify()
|
||||
} else {
|
||||
ctxu.GetLogger(context).Warnf("the ResponseWriter does not implement CloseNotifier (type: %T)", responseWriter)
|
||||
}
|
||||
|
||||
// Read in the data, if any.
|
||||
copied, err := io.Copy(destWriter, r.Body)
|
||||
if clientClosed != nil && (err != nil || (r.ContentLength > 0 && copied < r.ContentLength)) {
|
||||
// Didn't receive as much content as expected. Did the client
|
||||
// disconnect during the request? If so, avoid returning a 400
|
||||
// error to keep the logs cleaner.
|
||||
select {
|
||||
case <-clientClosed:
|
||||
// Set the response code to "499 Client Closed Request"
|
||||
// Even though the connection has already been closed,
|
||||
// this causes the logger to pick up a 499 error
|
||||
// instead of showing 0 for the HTTP status.
|
||||
responseWriter.WriteHeader(499)
|
||||
|
||||
ctxu.GetLoggerWithFields(context, map[interface{}]interface{}{
|
||||
"error": err,
|
||||
"copied": copied,
|
||||
"contentLength": r.ContentLength,
|
||||
}, "error", "copied", "contentLength").Error("client disconnected during " + action)
|
||||
return errors.New("client disconnected")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ctxu.GetLogger(context).Errorf("unknown error reading request payload: %v", err)
|
||||
*errSlice = append(*errSlice, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
72
vendor/github.com/docker/distribution/registry/handlers/hmac.go
generated
vendored
Normal file
72
vendor/github.com/docker/distribution/registry/handlers/hmac.go
generated
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// blobUploadState captures the state serializable state of the blob upload.
|
||||
type blobUploadState struct {
|
||||
// name is the primary repository under which the blob will be linked.
|
||||
Name string
|
||||
|
||||
// UUID identifies the upload.
|
||||
UUID string
|
||||
|
||||
// offset contains the current progress of the upload.
|
||||
Offset int64
|
||||
|
||||
// StartedAt is the original start time of the upload.
|
||||
StartedAt time.Time
|
||||
}
|
||||
|
||||
type hmacKey string
|
||||
|
||||
// unpackUploadState unpacks and validates the blob upload state from the
|
||||
// token, using the hmacKey secret.
|
||||
func (secret hmacKey) unpackUploadState(token string) (blobUploadState, error) {
|
||||
var state blobUploadState
|
||||
|
||||
tokenBytes, err := base64.URLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
|
||||
if len(tokenBytes) < mac.Size() {
|
||||
return state, fmt.Errorf("Invalid token")
|
||||
}
|
||||
|
||||
macBytes := tokenBytes[:mac.Size()]
|
||||
messageBytes := tokenBytes[mac.Size():]
|
||||
|
||||
mac.Write(messageBytes)
|
||||
if !hmac.Equal(mac.Sum(nil), macBytes) {
|
||||
return state, fmt.Errorf("Invalid token")
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(messageBytes, &state); err != nil {
|
||||
return state, err
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// packUploadState packs the upload state signed with and hmac digest using
|
||||
// the hmacKey secret, encoding to url safe base64. The resulting token can be
|
||||
// used to share data with minimized risk of external tampering.
|
||||
func (secret hmacKey) packUploadState(lus blobUploadState) (string, error) {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
p, err := json.Marshal(lus)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mac.Write(p)
|
||||
|
||||
return base64.URLEncoding.EncodeToString(append(mac.Sum(nil), p...)), nil
|
||||
}
|
||||
117
vendor/github.com/docker/distribution/registry/handlers/hmac_test.go
generated
vendored
Normal file
117
vendor/github.com/docker/distribution/registry/handlers/hmac_test.go
generated
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package handlers
|
||||
|
||||
import "testing"
|
||||
|
||||
var blobUploadStates = []blobUploadState{
|
||||
{
|
||||
Name: "hello",
|
||||
UUID: "abcd-1234-qwer-0987",
|
||||
Offset: 0,
|
||||
},
|
||||
{
|
||||
Name: "hello-world",
|
||||
UUID: "abcd-1234-qwer-0987",
|
||||
Offset: 0,
|
||||
},
|
||||
{
|
||||
Name: "h3ll0_w0rld",
|
||||
UUID: "abcd-1234-qwer-0987",
|
||||
Offset: 1337,
|
||||
},
|
||||
{
|
||||
Name: "ABCDEFG",
|
||||
UUID: "ABCD-1234-QWER-0987",
|
||||
Offset: 1234567890,
|
||||
},
|
||||
{
|
||||
Name: "this-is-A-sort-of-Long-name-for-Testing",
|
||||
UUID: "dead-1234-beef-0987",
|
||||
Offset: 8675309,
|
||||
},
|
||||
}
|
||||
|
||||
var secrets = []string{
|
||||
"supersecret",
|
||||
"12345",
|
||||
"a",
|
||||
"SuperSecret",
|
||||
"Sup3r... S3cr3t!",
|
||||
"This is a reasonably long secret key that is used for the purpose of testing.",
|
||||
"\u2603+\u2744", // snowman+snowflake
|
||||
}
|
||||
|
||||
// TestLayerUploadTokens constructs stateTokens from LayerUploadStates and
|
||||
// validates that the tokens can be used to reconstruct the proper upload state.
|
||||
func TestLayerUploadTokens(t *testing.T) {
|
||||
secret := hmacKey("supersecret")
|
||||
|
||||
for _, testcase := range blobUploadStates {
|
||||
token, err := secret.packUploadState(testcase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lus, err := secret.unpackUploadState(token)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assertBlobUploadStateEquals(t, testcase, lus)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHMACValidate ensures that any HMAC token providers are compatible if and
|
||||
// only if they share the same secret.
|
||||
func TestHMACValidation(t *testing.T) {
|
||||
for _, secret := range secrets {
|
||||
secret1 := hmacKey(secret)
|
||||
secret2 := hmacKey(secret)
|
||||
badSecret := hmacKey("DifferentSecret")
|
||||
|
||||
for _, testcase := range blobUploadStates {
|
||||
token, err := secret1.packUploadState(testcase)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lus, err := secret2.unpackUploadState(token)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assertBlobUploadStateEquals(t, testcase, lus)
|
||||
|
||||
_, err = badSecret.unpackUploadState(token)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected token provider to fail at retrieving state from token: %s", token)
|
||||
}
|
||||
|
||||
badToken, err := badSecret.packUploadState(lus)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = secret1.unpackUploadState(badToken)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected token provider to fail at retrieving state from token: %s", badToken)
|
||||
}
|
||||
|
||||
_, err = secret2.unpackUploadState(badToken)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected token provider to fail at retrieving state from token: %s", badToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertBlobUploadStateEquals(t *testing.T, expected blobUploadState, received blobUploadState) {
|
||||
if expected.Name != received.Name {
|
||||
t.Fatalf("Expected Name=%q, Received Name=%q", expected.Name, received.Name)
|
||||
}
|
||||
if expected.UUID != received.UUID {
|
||||
t.Fatalf("Expected UUID=%q, Received UUID=%q", expected.UUID, received.UUID)
|
||||
}
|
||||
if expected.Offset != received.Offset {
|
||||
t.Fatalf("Expected Offset=%d, Received Offset=%d", expected.Offset, received.Offset)
|
||||
}
|
||||
}
|
||||
53
vendor/github.com/docker/distribution/registry/handlers/hooks.go
generated
vendored
Normal file
53
vendor/github.com/docker/distribution/registry/handlers/hooks.go
generated
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// logHook is for hooking Panic in web application
|
||||
type logHook struct {
|
||||
LevelsParam []string
|
||||
Mail *mailer
|
||||
}
|
||||
|
||||
// Fire forwards an error to LogHook
|
||||
func (hook *logHook) Fire(entry *logrus.Entry) error {
|
||||
addr := strings.Split(hook.Mail.Addr, ":")
|
||||
if len(addr) != 2 {
|
||||
return errors.New("Invalid Mail Address")
|
||||
}
|
||||
host := addr[0]
|
||||
subject := fmt.Sprintf("[%s] %s: %s", entry.Level, host, entry.Message)
|
||||
|
||||
html := `
|
||||
{{.Message}}
|
||||
|
||||
{{range $key, $value := .Data}}
|
||||
{{$key}}: {{$value}}
|
||||
{{end}}
|
||||
`
|
||||
b := bytes.NewBuffer(make([]byte, 0))
|
||||
t := template.Must(template.New("mail body").Parse(html))
|
||||
if err := t.Execute(b, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
body := fmt.Sprintf("%s", b)
|
||||
|
||||
return hook.Mail.sendMail(subject, body)
|
||||
}
|
||||
|
||||
// Levels contains hook levels to be catched
|
||||
func (hook *logHook) Levels() []logrus.Level {
|
||||
levels := []logrus.Level{}
|
||||
for _, v := range hook.LevelsParam {
|
||||
lv, _ := logrus.ParseLevel(v)
|
||||
levels = append(levels, lv)
|
||||
}
|
||||
return levels
|
||||
}
|
||||
368
vendor/github.com/docker/distribution/registry/handlers/images.go
generated
vendored
Normal file
368
vendor/github.com/docker/distribution/registry/handlers/images.go
generated
vendored
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
ctxu "github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
// These constants determine which architecture and OS to choose from a
|
||||
// manifest list when downconverting it to a schema1 manifest.
|
||||
const (
|
||||
defaultArch = "amd64"
|
||||
defaultOS = "linux"
|
||||
)
|
||||
|
||||
// imageManifestDispatcher takes the request context and builds the
|
||||
// appropriate handler for handling image manifest requests.
|
||||
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||
imageManifestHandler := &imageManifestHandler{
|
||||
Context: ctx,
|
||||
}
|
||||
reference := getReference(ctx)
|
||||
dgst, err := digest.ParseDigest(reference)
|
||||
if err != nil {
|
||||
// We just have a tag
|
||||
imageManifestHandler.Tag = reference
|
||||
} else {
|
||||
imageManifestHandler.Digest = dgst
|
||||
}
|
||||
|
||||
mhandler := handlers.MethodHandler{
|
||||
"GET": http.HandlerFunc(imageManifestHandler.GetImageManifest),
|
||||
"HEAD": http.HandlerFunc(imageManifestHandler.GetImageManifest),
|
||||
}
|
||||
|
||||
if !ctx.readOnly {
|
||||
mhandler["PUT"] = http.HandlerFunc(imageManifestHandler.PutImageManifest)
|
||||
mhandler["DELETE"] = http.HandlerFunc(imageManifestHandler.DeleteImageManifest)
|
||||
}
|
||||
|
||||
return mhandler
|
||||
}
|
||||
|
||||
// imageManifestHandler handles http operations on image manifests.
|
||||
type imageManifestHandler struct {
|
||||
*Context
|
||||
|
||||
// One of tag or digest gets set, depending on what is present in context.
|
||||
Tag string
|
||||
Digest digest.Digest
|
||||
}
|
||||
|
||||
// GetImageManifest fetches the image manifest from the storage backend, if it exists.
|
||||
func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) {
|
||||
ctxu.GetLogger(imh).Debug("GetImageManifest")
|
||||
manifests, err := imh.Repository.Manifests(imh)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, err)
|
||||
return
|
||||
}
|
||||
|
||||
var manifest distribution.Manifest
|
||||
if imh.Tag != "" {
|
||||
tags := imh.Repository.Tags(imh)
|
||||
desc, err := tags.Get(imh, imh.Tag)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
imh.Digest = desc.Digest
|
||||
}
|
||||
|
||||
if etagMatch(r, imh.Digest.String()) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
var options []distribution.ManifestServiceOption
|
||||
if imh.Tag != "" {
|
||||
options = append(options, distribution.WithTag(imh.Tag))
|
||||
}
|
||||
manifest, err = manifests.Get(imh, imh.Digest, options...)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
|
||||
supportsSchema2 := false
|
||||
supportsManifestList := false
|
||||
if acceptHeaders, ok := r.Header["Accept"]; ok {
|
||||
for _, mediaType := range acceptHeaders {
|
||||
if mediaType == schema2.MediaTypeManifest {
|
||||
supportsSchema2 = true
|
||||
}
|
||||
if mediaType == manifestlist.MediaTypeManifestList {
|
||||
supportsManifestList = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest)
|
||||
manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList)
|
||||
|
||||
// Only rewrite schema2 manifests when they are being fetched by tag.
|
||||
// If they are being fetched by digest, we can't return something not
|
||||
// matching the digest.
|
||||
if imh.Tag != "" && isSchema2 && !supportsSchema2 {
|
||||
// Rewrite manifest in schema1 format
|
||||
ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String())
|
||||
|
||||
manifest, err = imh.convertSchema2Manifest(schema2Manifest)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if imh.Tag != "" && isManifestList && !supportsManifestList {
|
||||
// Rewrite manifest in schema1 format
|
||||
ctxu.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String())
|
||||
|
||||
// Find the image manifest corresponding to the default
|
||||
// platform
|
||||
var manifestDigest digest.Digest
|
||||
for _, manifestDescriptor := range manifestList.Manifests {
|
||||
if manifestDescriptor.Platform.Architecture == defaultArch && manifestDescriptor.Platform.OS == defaultOS {
|
||||
manifestDigest = manifestDescriptor.Digest
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if manifestDigest == "" {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
manifest, err = manifests.Get(imh, manifestDigest)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
|
||||
// If necessary, convert the image manifest
|
||||
if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 {
|
||||
manifest, err = imh.convertSchema2Manifest(schema2Manifest)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ct, p, err := manifest.Payload()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", ct)
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(p)))
|
||||
w.Header().Set("Docker-Content-Digest", imh.Digest.String())
|
||||
w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
|
||||
w.Write(p)
|
||||
}
|
||||
|
||||
func (imh *imageManifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) {
|
||||
targetDescriptor := schema2Manifest.Target()
|
||||
blobs := imh.Repository.Blobs(imh)
|
||||
configJSON, err := blobs.Get(imh, targetDescriptor.Digest)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref := imh.Repository.Named()
|
||||
|
||||
if imh.Tag != "" {
|
||||
ref, err = reference.WithTag(ref, imh.Tag)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, ref, configJSON)
|
||||
for _, d := range schema2Manifest.References() {
|
||||
if err := builder.AppendReference(d); err != nil {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
manifest, err := builder.Build(imh)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
|
||||
return nil, err
|
||||
}
|
||||
imh.Digest = digest.FromBytes(manifest.(*schema1.SignedManifest).Canonical)
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func etagMatch(r *http.Request, etag string) bool {
|
||||
for _, headerVal := range r.Header["If-None-Match"] {
|
||||
if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PutImageManifest validates and stores an image in the registry.
|
||||
func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) {
|
||||
ctxu.GetLogger(imh).Debug("PutImageManifest")
|
||||
manifests, err := imh.Repository.Manifests(imh)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, err)
|
||||
return
|
||||
}
|
||||
|
||||
var jsonBuf bytes.Buffer
|
||||
if err := copyFullPayload(w, r, &jsonBuf, imh, "image manifest PUT", &imh.Errors); err != nil {
|
||||
// copyFullPayload reports the error if necessary
|
||||
return
|
||||
}
|
||||
|
||||
mediaType := r.Header.Get("Content-Type")
|
||||
manifest, desc, err := distribution.UnmarshalManifest(mediaType, jsonBuf.Bytes())
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err))
|
||||
return
|
||||
}
|
||||
|
||||
if imh.Digest != "" {
|
||||
if desc.Digest != imh.Digest {
|
||||
ctxu.GetLogger(imh).Errorf("payload digest does match: %q != %q", desc.Digest, imh.Digest)
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
|
||||
return
|
||||
}
|
||||
} else if imh.Tag != "" {
|
||||
imh.Digest = desc.Digest
|
||||
} else {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified"))
|
||||
return
|
||||
}
|
||||
|
||||
var options []distribution.ManifestServiceOption
|
||||
if imh.Tag != "" {
|
||||
options = append(options, distribution.WithTag(imh.Tag))
|
||||
}
|
||||
_, err = manifests.Put(imh, manifest, options...)
|
||||
if err != nil {
|
||||
// TODO(stevvooe): These error handling switches really need to be
|
||||
// handled by an app global mapper.
|
||||
if err == distribution.ErrUnsupported {
|
||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
|
||||
return
|
||||
}
|
||||
if err == distribution.ErrAccessDenied {
|
||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeDenied)
|
||||
return
|
||||
}
|
||||
switch err := err.(type) {
|
||||
case distribution.ErrManifestVerification:
|
||||
for _, verificationError := range err {
|
||||
switch verificationError := verificationError.(type) {
|
||||
case distribution.ErrManifestBlobUnknown:
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestBlobUnknown.WithDetail(verificationError.Digest))
|
||||
case distribution.ErrManifestNameInvalid:
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeNameInvalid.WithDetail(err))
|
||||
case distribution.ErrManifestUnverified:
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnverified)
|
||||
default:
|
||||
if verificationError == digest.ErrDigestInvalidFormat {
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
|
||||
} else {
|
||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown, verificationError)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Tag this manifest
|
||||
if imh.Tag != "" {
|
||||
tags := imh.Repository.Tags(imh)
|
||||
err = tags.Tag(imh, imh.Tag, desc)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Construct a canonical url for the uploaded manifest.
|
||||
ref, err := reference.WithDigest(imh.Repository.Named(), imh.Digest)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
|
||||
location, err := imh.urlBuilder.BuildManifestURL(ref)
|
||||
if err != nil {
|
||||
// NOTE(stevvooe): Given the behavior above, this absurdly unlikely to
|
||||
// happen. We'll log the error here but proceed as if it worked. Worst
|
||||
// case, we set an empty location header.
|
||||
ctxu.GetLogger(imh).Errorf("error building manifest url from digest: %v", err)
|
||||
}
|
||||
|
||||
w.Header().Set("Location", location)
|
||||
w.Header().Set("Docker-Content-Digest", imh.Digest.String())
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
// DeleteImageManifest removes the manifest with the given digest from the registry.
|
||||
func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) {
|
||||
ctxu.GetLogger(imh).Debug("DeleteImageManifest")
|
||||
|
||||
manifests, err := imh.Repository.Manifests(imh)
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = manifests.Delete(imh, imh.Digest)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case digest.ErrDigestUnsupported:
|
||||
case digest.ErrDigestInvalidFormat:
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid)
|
||||
return
|
||||
case distribution.ErrBlobUnknown:
|
||||
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
|
||||
return
|
||||
case distribution.ErrUnsupported:
|
||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported)
|
||||
return
|
||||
default:
|
||||
imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tagService := imh.Repository.Tags(imh)
|
||||
referencedTags, err := tagService.Lookup(imh, distribution.Descriptor{Digest: imh.Digest})
|
||||
if err != nil {
|
||||
imh.Errors = append(imh.Errors, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, tag := range referencedTags {
|
||||
if err := tagService.Untag(imh, tag); err != nil {
|
||||
imh.Errors = append(imh.Errors, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
45
vendor/github.com/docker/distribution/registry/handlers/mail.go
generated
vendored
Normal file
45
vendor/github.com/docker/distribution/registry/handlers/mail.go
generated
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// mailer provides fields of email configuration for sending.
|
||||
type mailer struct {
|
||||
Addr, Username, Password, From string
|
||||
Insecure bool
|
||||
To []string
|
||||
}
|
||||
|
||||
// sendMail allows users to send email, only if mail parameters is configured correctly.
|
||||
func (mail *mailer) sendMail(subject, message string) error {
|
||||
addr := strings.Split(mail.Addr, ":")
|
||||
if len(addr) != 2 {
|
||||
return errors.New("Invalid Mail Address")
|
||||
}
|
||||
host := addr[0]
|
||||
msg := []byte("To:" + strings.Join(mail.To, ";") +
|
||||
"\r\nFrom: " + mail.From +
|
||||
"\r\nSubject: " + subject +
|
||||
"\r\nContent-Type: text/plain\r\n\r\n" +
|
||||
message)
|
||||
auth := smtp.PlainAuth(
|
||||
"",
|
||||
mail.Username,
|
||||
mail.Password,
|
||||
host,
|
||||
)
|
||||
err := smtp.SendMail(
|
||||
mail.Addr,
|
||||
auth,
|
||||
mail.From,
|
||||
mail.To,
|
||||
[]byte(msg),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
60
vendor/github.com/docker/distribution/registry/handlers/tags.go
generated
vendored
Normal file
60
vendor/github.com/docker/distribution/registry/handlers/tags.go
generated
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/gorilla/handlers"
|
||||
)
|
||||
|
||||
// tagsDispatcher constructs the tags handler api endpoint.
|
||||
func tagsDispatcher(ctx *Context, r *http.Request) http.Handler {
|
||||
tagsHandler := &tagsHandler{
|
||||
Context: ctx,
|
||||
}
|
||||
|
||||
return handlers.MethodHandler{
|
||||
"GET": http.HandlerFunc(tagsHandler.GetTags),
|
||||
}
|
||||
}
|
||||
|
||||
// tagsHandler handles requests for lists of tags under a repository name.
|
||||
type tagsHandler struct {
|
||||
*Context
|
||||
}
|
||||
|
||||
type tagsAPIResponse struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// GetTags returns a json list of tags for a specific image name.
|
||||
func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
tagService := th.Repository.Tags(th)
|
||||
tags, err := tagService.All(th)
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
case distribution.ErrRepositoryUnknown:
|
||||
th.Errors = append(th.Errors, v2.ErrorCodeNameUnknown.WithDetail(map[string]string{"name": th.Repository.Named().Name()}))
|
||||
default:
|
||||
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
if err := enc.Encode(tagsAPIResponse{
|
||||
Name: th.Repository.Named().Name(),
|
||||
Tags: tags,
|
||||
}); err != nil {
|
||||
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue