mirror of
https://github.com/Quiq/docker-registry-ui.git
synced 2025-09-06 07:30:00 +00:00
Initial commit.
This commit is contained in:
245
registry/client.go
Normal file
245
registry/client.go
Normal file
@@ -0,0 +1,245 @@
|
||||
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(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) (rdata, rdigest string) {
|
||||
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 "", ""
|
||||
}
|
||||
|
||||
c.logger.Info("GET ", uri, " ", resp.Status)
|
||||
// Returns 404 when no tags in the repo.
|
||||
if resp.StatusCode != 200 {
|
||||
return "", ""
|
||||
}
|
||||
digest := resp.Header.Get("Docker-Content-Digest")
|
||||
|
||||
if delete {
|
||||
// Delete by manifest digest reference.
|
||||
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 "", ""
|
||||
}
|
||||
|
||||
return data, digest
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
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()
|
||||
scope := "registry:catalog:*"
|
||||
data, _ := c.callRegistry("/v2/_catalog", scope, 2, false)
|
||||
if data == "" {
|
||||
return c.repos
|
||||
}
|
||||
|
||||
c.repos = map[string][]string{}
|
||||
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)
|
||||
}
|
||||
|
||||
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, digest := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, false)
|
||||
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)
|
||||
}
|
Reference in New Issue
Block a user