docker-registry-ui/registry/client.go
2019-02-19 16:41:46 +02:00

265 lines
7.2 KiB
Go

package registry
import (
"crypto/tls"
"fmt"
"regexp"
"sort"
"strings"
"sync"
"time"
"github.com/hhkbp2/go-logging"
"github.com/parnurzeal/gorequest"
"github.com/tidwall/gjson"
)
// Client main class.
type Client struct {
url string
verifyTLS bool
username string
password string
request *gorequest.SuperAgent
logger logging.Logger
mux sync.Mutex
tokens map[string]string
repos map[string][]string
tagCounts map[string]int
authURL string
}
// NewClient initialize Client.
func NewClient(url string, verifyTLS bool, username, password string) *Client {
c := &Client{
url: strings.TrimRight(url, "/"),
verifyTLS: verifyTLS,
username: username,
password: password,
request: gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: !verifyTLS}),
logger: SetupLogging("registry.client"),
tokens: map[string]string{},
repos: map[string][]string{},
tagCounts: map[string]int{},
}
resp, _, errs := c.request.Get(c.url+"/v2/").Set("User-Agent", "docker-registry-ui").End()
if len(errs) > 0 {
c.logger.Error(errs[0])
return nil
}
authHeader := ""
if resp.StatusCode == 200 {
return c
} else if resp.StatusCode == 401 {
authHeader = resp.Header.Get("WWW-Authenticate")
} else {
c.logger.Error(resp.Status)
return nil
}
if strings.HasPrefix(authHeader, "Bearer") {
r, _ := regexp.Compile(`^Bearer realm="(http.+)",service="(.+)"`)
if m := r.FindStringSubmatch(authHeader); len(m) > 0 {
c.authURL = fmt.Sprintf("%s?service=%s", m[1], m[2])
c.logger.Info("Token auth service discovered at ", c.authURL)
}
if c.authURL == "" {
c.logger.Warn("No token auth service discovered from ", c.url)
return nil
}
} else if strings.HasPrefix(strings.ToLower(authHeader), "basic") {
c.request = c.request.SetBasicAuth(c.username, c.password)
c.logger.Info("It was discovered the registry is configured with HTTP basic auth.")
}
return c
}
// getToken get existing or new auth token.
func (c *Client) getToken(scope string) string {
// Check if we have already a token and it's not expired.
if token, ok := c.tokens[scope]; ok {
resp, _, _ := c.request.Get(c.url+"/v2/").Set("Authorization", fmt.Sprintf("Bearer %s", token)).Set("User-Agent", "docker-registry-ui").End()
if resp != nil && resp.StatusCode == 200 {
return token
}
}
request := gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: !c.verifyTLS})
resp, data, errs := request.Get(fmt.Sprintf("%s&scope=%s", c.authURL, scope)).SetBasicAuth(c.username, c.password).Set("User-Agent", "docker-registry-ui").End()
if len(errs) > 0 {
c.logger.Error(errs[0])
return ""
}
if resp.StatusCode != 200 {
c.logger.Error("Failed to get token for scope ", scope, " from ", c.authURL)
return ""
}
c.tokens[scope] = gjson.Get(data, "token").String()
c.logger.Info("Received new token for scope ", scope)
return c.tokens[scope]
}
// callRegistry make an HTTP request to Docker registry.
func (c *Client) callRegistry(uri, scope string, manifest uint, delete bool) (string, gorequest.Response) {
acceptHeader := fmt.Sprintf("application/vnd.docker.distribution.manifest.v%d+json", manifest)
authHeader := ""
if c.authURL != "" {
authHeader = fmt.Sprintf("Bearer %s", c.getToken(scope))
}
resp, data, errs := c.request.Get(c.url+uri).Set("Accept", acceptHeader).Set("Authorization", authHeader).Set("User-Agent", "docker-registry-ui").End()
if len(errs) > 0 {
c.logger.Error(errs[0])
return "", resp
}
c.logger.Info("GET ", uri, " ", resp.Status)
// Returns 404 when no tags in the repo.
if resp.StatusCode != 200 {
return "", resp
}
if delete {
// Delete by manifest digest reference.
digest := resp.Header.Get("Docker-Content-Digest")
parts := strings.Split(uri, "/manifests/")
uri = parts[0] + "/manifests/" + digest
resp, _, errs := c.request.Delete(c.url+uri).Set("Accept", acceptHeader).Set("Authorization", authHeader).Set("User-Agent", "docker-registry-ui").End()
if len(errs) > 0 {
c.logger.Error(errs[0])
} else {
// Returns 202 on success.
c.logger.Info("DELETE ", uri, " (", parts[1], ") ", resp.Status)
}
return "", resp
}
return data, resp
}
// Namespaces list repo namespaces.
func (c *Client) Namespaces() []string {
namespaces := make([]string, 0, len(c.repos))
for k := range c.repos {
namespaces = append(namespaces, k)
}
if !ItemInSlice("library", namespaces) {
namespaces = append(namespaces, "library")
}
sort.Strings(namespaces)
return namespaces
}
// Repositories list repos by namespaces where 'library' is the default one.
func (c *Client) Repositories(useCache bool) map[string][]string {
// Return from cache if available.
if len(c.repos) > 0 && useCache {
return c.repos
}
c.mux.Lock()
defer c.mux.Unlock()
linkRegexp := regexp.MustCompile("^<(.*?)>;.*$")
scope := "registry:catalog:*"
uri := "/v2/_catalog"
c.repos = map[string][]string{}
for {
data, resp := c.callRegistry(uri, scope, 2, false)
if data == "" {
return c.repos
}
for _, r := range gjson.Get(data, "repositories").Array() {
namespace := "library"
repo := r.String()
if strings.Contains(repo, "/") {
f := strings.SplitN(repo, "/", 2)
namespace = f[0]
repo = f[1]
}
c.repos[namespace] = append(c.repos[namespace], repo)
}
// pagination
linkHeader := resp.Header.Get("Link")
link := linkRegexp.FindStringSubmatch(linkHeader)
if len(link) == 2 {
// update uri and query next page
uri = link[1]
} else {
// no more pages
break
}
}
return c.repos
}
// Tags get tags for the repo.
func (c *Client) Tags(repo string) []string {
scope := fmt.Sprintf("repository:%s:*", repo)
data, _ := c.callRegistry(fmt.Sprintf("/v2/%s/tags/list", repo), scope, 2, false)
var tags []string
for _, t := range gjson.Get(data, "tags").Array() {
tags = append(tags, t.String())
}
return tags
}
// TagInfo get image info for the repo tag.
func (c *Client) TagInfo(repo, tag string, v1only bool) (rsha256, rinfoV1, rinfoV2 string) {
scope := fmt.Sprintf("repository:%s:*", repo)
infoV1, _ := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 1, false)
if infoV1 == "" {
return "", "", ""
}
if v1only {
return "", infoV1, ""
}
infoV2, resp := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, false)
digest := resp.Header.Get("Docker-Content-Digest")
if infoV2 == "" || digest == "" {
return "", "", ""
}
sha256 := digest[7:]
return sha256, infoV1, infoV2
}
// TagCounts return map with tag counts.
func (c *Client) TagCounts() map[string]int {
return c.tagCounts
}
// CountTags count repository tags in background regularly.
func (c *Client) CountTags(interval uint8) {
for {
c.logger.Info("Calculating tags in background...")
catalog := c.Repositories(false)
for n, repos := range catalog {
for _, r := range repos {
repoPath := r
if n != "library" {
repoPath = fmt.Sprintf("%s/%s", n, r)
}
c.tagCounts[fmt.Sprintf("%s/%s", n, r)] = len(c.Tags(repoPath))
}
}
c.logger.Info("Tags calculation complete.")
time.Sleep(time.Duration(interval) * time.Minute)
}
}
// DeleteTag delete image tag.
func (c *Client) DeleteTag(repo, tag string) {
scope := fmt.Sprintf("repository:%s:*", repo)
c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, true)
}