diff --git a/registry/registry.go b/registry/registry.go index fc84f19ec4..e6f4f592e2 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -98,6 +98,13 @@ func ResolveRepositoryName(reposName string) (string, string, error) { return endpoint, reposName, err } +// VersionInfo is used to model entities which has a version. +// It is basically a tupple with name and version. +type VersionInfo interface { + Name() string + Version() string +} + func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) { for _, cookie := range c.Jar.Cookies(req.URL) { req.AddCookie(cookie) @@ -105,6 +112,20 @@ func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) { return c.Do(req) } +// Set the user agent field in the header based on the versions provided +// in NewRegistry() and extra. +func (r *Registry) setUserAgent(req *http.Request, extra ...VersionInfo) { + if len(r.baseVersions)+len(extra) == 0 { + return + } + if len(extra) == 0 { + req.Header.Set("User-Agent", r.baseVersionsStr) + } else { + req.Header.Set("User-Agent", appendVersions(r.baseVersionsStr, extra...)) + } + return +} + // Retrieve the history of a given image from the Registry. // Return a list of the parent's json (requested image included) func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]string, error) { @@ -113,6 +134,7 @@ func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]s return nil, err } req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) + r.setUserAgent(req) res, err := r.client.Do(req) if err != nil || res.StatusCode != 200 { if res != nil { @@ -159,6 +181,7 @@ func (r *Registry) GetRemoteImageJSON(imgID, registry string, token []string) ([ return nil, -1, fmt.Errorf("Failed to download json: %s", err) } req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) + r.setUserAgent(req) res, err := r.client.Do(req) if err != nil { return nil, -1, fmt.Errorf("Failed to download json: %s", err) @@ -186,6 +209,7 @@ func (r *Registry) GetRemoteImageLayer(imgID, registry string, token []string) ( return nil, fmt.Errorf("Error while getting from the server: %s\n", err) } req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) + r.setUserAgent(req) res, err := r.client.Do(req) if err != nil { return nil, err @@ -206,6 +230,7 @@ func (r *Registry) GetRemoteTags(registries []string, repository string, token [ return nil, err } req.Header.Set("Authorization", "Token "+strings.Join(token, ", ")) + r.setUserAgent(req) res, err := r.client.Do(req) if err != nil { return nil, err @@ -244,6 +269,7 @@ func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, e req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) } req.Header.Set("X-Docker-Token", "true") + r.setUserAgent(req) res, err := r.client.Do(req) if err != nil { @@ -307,6 +333,7 @@ func (r *Registry) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, regis req.Header.Add("Content-type", "application/json") req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) req.Header.Set("X-Docker-Checksum", imgData.Checksum) + r.setUserAgent(req) utils.Debugf("Setting checksum for %s: %s", imgData.ID, imgData.Checksum) res, err := doWithCookies(r.client, req) @@ -341,6 +368,7 @@ func (r *Registry) PushImageLayerRegistry(imgID string, layer io.Reader, registr req.ContentLength = -1 req.TransferEncoding = []string{"chunked"} req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) + r.setUserAgent(req) res, err := doWithCookies(r.client, req) if err != nil { return fmt.Errorf("Failed to upload layer: %s", err) @@ -378,6 +406,7 @@ func (r *Registry) PushRegistryTag(remote, revision, tag, registry string, token } req.Header.Add("Content-type", "application/json") req.Header.Set("Authorization", "Token "+strings.Join(token, ",")) + r.setUserAgent(req) req.ContentLength = int64(len(revision)) res, err := doWithCookies(r.client, req) if err != nil { @@ -410,6 +439,7 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) req.ContentLength = int64(len(imgListJSON)) req.Header.Set("X-Docker-Token", "true") + r.setUserAgent(req) if validate { req.Header["X-Docker-Endpoints"] = regs } @@ -430,6 +460,7 @@ func (r *Registry) PushImageJSONIndex(indexEp, remote string, imgList []*ImgData req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password) req.ContentLength = int64(len(imgListJSON)) req.Header.Set("X-Docker-Token", "true") + r.setUserAgent(req) if validate { req.Header["X-Docker-Endpoints"] = regs } @@ -536,11 +567,52 @@ type ImgData struct { } type Registry struct { - client *http.Client - authConfig *auth.AuthConfig + client *http.Client + authConfig *auth.AuthConfig + baseVersions []VersionInfo + baseVersionsStr string } -func NewRegistry(root string, authConfig *auth.AuthConfig) (r *Registry, err error) { +func validVersion(version VersionInfo) bool { + stopChars := " \t\r\n/" + if strings.ContainsAny(version.Name(), stopChars) { + return false + } + if strings.ContainsAny(version.Version(), stopChars) { + return false + } + return true +} + +// Convert versions to a string and append the string to the string base. +// +// Each VersionInfo will be converted to a string in the format of +// "product/version", where the "product" is get from the Name() method, while +// version is get from the Version() method. Several pieces of verson information +// will be concatinated and separated by space. +func appendVersions(base string, versions ...VersionInfo) string { + if len(versions) == 0 { + return base + } + + var buf bytes.Buffer + if len(base) > 0 { + buf.Write([]byte(base)) + } + + for _, v := range versions { + if !validVersion(v) { + continue + } + buf.Write([]byte(v.Name())) + buf.Write([]byte("/")) + buf.Write([]byte(v.Version())) + buf.Write([]byte(" ")) + } + return buf.String() +} + +func NewRegistry(root string, authConfig *auth.AuthConfig, baseVersions ...VersionInfo) (r *Registry, err error) { httpTransport := &http.Transport{ DisableKeepAlives: true, Proxy: http.ProxyFromEnvironment, @@ -553,5 +625,10 @@ func NewRegistry(root string, authConfig *auth.AuthConfig) (r *Registry, err err }, } r.client.Jar, err = cookiejar.New(nil) - return r, err + if err != nil { + return nil, err + } + r.baseVersions = baseVersions + r.baseVersionsStr = appendVersions("", baseVersions...) + return r, nil } diff --git a/server.go b/server.go index 8eff17a947..4179a1e160 100644 --- a/server.go +++ b/server.go @@ -30,6 +30,47 @@ func (srv *Server) DockerVersion() APIVersion { } } +// simpleVersionInfo is a simple implementation of +// the interface VersionInfo, which is used +// to provide version information for some product, +// component, etc. It stores the product name and the version +// in string and returns them on calls to Name() and Version(). +type simpleVersionInfo struct { + name string + version string +} + +func (v *simpleVersionInfo) Name() string { + return v.name +} + +func (v *simpleVersionInfo) Version() string { + return v.version +} + +// versionCheckers() returns version informations of: +// docker, go, git-commit (of the docker) and the host's kernel. +// +// Such information will be used on call to NewRegistry(). +func (srv *Server) versionInfos() []registry.VersionInfo { + v := srv.DockerVersion() + ret := make([]registry.VersionInfo, 0, 4) + ret = append(ret, &simpleVersionInfo{"docker", v.Version}) + + if len(v.GoVersion) > 0 { + ret = append(ret, &simpleVersionInfo{"go", v.GoVersion}) + } + if len(v.GitCommit) > 0 { + ret = append(ret, &simpleVersionInfo{"git-commit", v.GitCommit}) + } + kernelVersion, err := utils.GetKernelVersion() + if err == nil { + ret = append(ret, &simpleVersionInfo{"kernel", kernelVersion.String()}) + } + + return ret +} + func (srv *Server) ContainerKill(name string) error { if container := srv.runtime.Get(name); container != nil { if err := container.Kill(); err != nil { @@ -61,7 +102,7 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error { } func (srv *Server) ImagesSearch(term string) ([]APISearch, error) { - r, err := registry.NewRegistry(srv.runtime.root, nil) + r, err := registry.NewRegistry(srv.runtime.root, nil, srv.versionInfos()...) if err != nil { return nil, err } @@ -511,7 +552,7 @@ func (srv *Server) poolRemove(kind, key string) error { } func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error { - r, err := registry.NewRegistry(srv.runtime.root, authConfig) + r, err := registry.NewRegistry(srv.runtime.root, authConfig, srv.versionInfos()...) if err != nil { return err } @@ -728,7 +769,7 @@ func (srv *Server) ImagePush(localName string, out io.Writer, sf *utils.StreamFo out = utils.NewWriteFlusher(out) img, err := srv.runtime.graph.Get(localName) - r, err2 := registry.NewRegistry(srv.runtime.root, authConfig) + r, err2 := registry.NewRegistry(srv.runtime.root, authConfig, srv.versionInfos()...) if err2 != nil { return err2 }