mirror of
https://github.com/project-zot/zot.git
synced 2024-12-23 22:27:35 -05:00
258 lines
6 KiB
Go
258 lines
6 KiB
Go
|
//go:build sync
|
||
|
// +build sync
|
||
|
|
||
|
package sync
|
||
|
|
||
|
import (
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/Masterminds/semver"
|
||
|
glob "github.com/bmatcuk/doublestar/v4"
|
||
|
|
||
|
"zotregistry.io/zot/pkg/common"
|
||
|
syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
|
||
|
"zotregistry.io/zot/pkg/log"
|
||
|
)
|
||
|
|
||
|
/* ContentManager uses registry content configuration to filter repos/tags
|
||
|
and also manages applying destination/stripPrefix rules
|
||
|
eg: "content": [
|
||
|
{
|
||
|
"prefix": "/repo1/repo",
|
||
|
"destination": "/repo",
|
||
|
"stripPrefix": true
|
||
|
"tags": {
|
||
|
"regex": "4.*",
|
||
|
"semver": true
|
||
|
}
|
||
|
}
|
||
|
]
|
||
|
*/
|
||
|
|
||
|
type ContentManager struct {
|
||
|
contents []syncconf.Content
|
||
|
log log.Logger
|
||
|
}
|
||
|
|
||
|
func NewContentManager(contents []syncconf.Content, log log.Logger) ContentManager {
|
||
|
return ContentManager{contents: contents, log: log}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
MatchesContent returns whether a repo matches a registry
|
||
|
config content (is not filtered out by content config rules).
|
||
|
*/
|
||
|
func (cm ContentManager) MatchesContent(repo string) bool {
|
||
|
content := cm.getContentByUpstreamRepo(repo)
|
||
|
|
||
|
return content != nil
|
||
|
}
|
||
|
|
||
|
// FilterTags filters a repo tags based on content config rules (semver, regex).
|
||
|
func (cm ContentManager) FilterTags(repo string, tags []string) ([]string, error) {
|
||
|
content := cm.getContentByLocalRepo(repo)
|
||
|
|
||
|
var err error
|
||
|
// filter based on tags rules
|
||
|
if content != nil && content.Tags != nil {
|
||
|
if content.Tags.Regex != nil {
|
||
|
tags, err = filterTagsByRegex(tags, *content.Tags.Regex, cm.log)
|
||
|
if err != nil {
|
||
|
return []string{}, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if content.Tags.Semver != nil && *content.Tags.Semver {
|
||
|
tags = filterTagsBySemver(tags, cm.log)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return tags, nil
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
GetRepoDestination applies content destination config rule and returns the final repo namespace.
|
||
|
- used by periodically sync.
|
||
|
*/
|
||
|
func (cm ContentManager) GetRepoDestination(repo string) string {
|
||
|
content := cm.getContentByUpstreamRepo(repo)
|
||
|
if content == nil {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
return getRepoDestination(repo, *content)
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
GetRepoSource is the inverse function of GetRepoDestination, needed in on demand to find out
|
||
|
the remote name of a repo given a local repo.
|
||
|
- used by on demand sync.
|
||
|
*/
|
||
|
func (cm ContentManager) GetRepoSource(repo string) string {
|
||
|
content := cm.getContentByLocalRepo(repo)
|
||
|
if content == nil {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
return getRepoSource(repo, *content)
|
||
|
}
|
||
|
|
||
|
// utilies functions.
|
||
|
func (cm ContentManager) getContentByUpstreamRepo(repo string) *syncconf.Content {
|
||
|
for _, content := range cm.contents {
|
||
|
var prefix string
|
||
|
// handle prefixes starting with '/'
|
||
|
if strings.HasPrefix(content.Prefix, "/") {
|
||
|
prefix = content.Prefix[1:]
|
||
|
} else {
|
||
|
prefix = content.Prefix
|
||
|
}
|
||
|
|
||
|
matched, err := glob.Match(prefix, repo)
|
||
|
if err != nil {
|
||
|
cm.log.Error().Str("errorType", common.TypeOf(err)).
|
||
|
Err(err).Str("pattern",
|
||
|
prefix).Msg("error while parsing glob pattern, skipping it...")
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if matched {
|
||
|
return &content
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (cm ContentManager) getContentByLocalRepo(repo string) *syncconf.Content {
|
||
|
contentID := -1
|
||
|
repo = strings.Trim(repo, "/")
|
||
|
|
||
|
for cID, content := range cm.contents {
|
||
|
// make sure prefix ends in "/" to extract the meta characters
|
||
|
prefix := strings.Trim(content.Prefix, "/") + "/"
|
||
|
destination := strings.Trim(content.Destination, "/")
|
||
|
|
||
|
var patternSlice []string
|
||
|
|
||
|
if content.StripPrefix {
|
||
|
_, metaCharacters := glob.SplitPattern(prefix)
|
||
|
patternSlice = append(patternSlice, destination, metaCharacters)
|
||
|
} else {
|
||
|
patternSlice = append(patternSlice, destination, prefix)
|
||
|
}
|
||
|
|
||
|
pattern := strings.Trim(strings.Join(patternSlice, "/"), "/")
|
||
|
|
||
|
matched, err := glob.Match(pattern, repo)
|
||
|
if err != nil {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if matched {
|
||
|
contentID = cID
|
||
|
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if contentID == -1 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return &cm.contents[contentID]
|
||
|
}
|
||
|
|
||
|
func getRepoSource(localRepo string, content syncconf.Content) string {
|
||
|
localRepo = strings.Trim(localRepo, "/")
|
||
|
destination := strings.Trim(content.Destination, "/")
|
||
|
prefix := strings.Trim(content.Prefix, "/*")
|
||
|
|
||
|
var localRepoSlice []string
|
||
|
|
||
|
localRepo = strings.TrimPrefix(localRepo, destination)
|
||
|
localRepo = strings.Trim(localRepo, "/")
|
||
|
|
||
|
if content.StripPrefix {
|
||
|
localRepoSlice = append([]string{prefix}, localRepo)
|
||
|
} else {
|
||
|
localRepoSlice = []string{localRepo}
|
||
|
}
|
||
|
|
||
|
repoSource := strings.Join(localRepoSlice, "/")
|
||
|
if repoSource == "/" {
|
||
|
return repoSource
|
||
|
}
|
||
|
|
||
|
return strings.Trim(repoSource, "/")
|
||
|
}
|
||
|
|
||
|
// getRepoDestination returns the local storage path of the synced repo based on the specified destination.
|
||
|
func getRepoDestination(remoteRepo string, content syncconf.Content) string {
|
||
|
remoteRepo = strings.Trim(remoteRepo, "/")
|
||
|
destination := strings.Trim(content.Destination, "/")
|
||
|
prefix := strings.Trim(content.Prefix, "/*")
|
||
|
|
||
|
var repoDestSlice []string
|
||
|
|
||
|
if content.StripPrefix {
|
||
|
remoteRepo = strings.TrimPrefix(remoteRepo, prefix)
|
||
|
remoteRepo = strings.Trim(remoteRepo, "/")
|
||
|
repoDestSlice = append(repoDestSlice, destination, remoteRepo)
|
||
|
} else {
|
||
|
repoDestSlice = append(repoDestSlice, destination, remoteRepo)
|
||
|
}
|
||
|
|
||
|
repoDestination := strings.Join(repoDestSlice, "/")
|
||
|
|
||
|
if repoDestination == "/" {
|
||
|
return "/"
|
||
|
}
|
||
|
|
||
|
return strings.Trim(repoDestination, "/")
|
||
|
}
|
||
|
|
||
|
// filterTagsByRegex filters images by tag regex given in the config.
|
||
|
func filterTagsByRegex(tags []string, regex string, log log.Logger) ([]string, error) {
|
||
|
filteredTags := []string{}
|
||
|
|
||
|
if len(tags) == 0 || regex == "" {
|
||
|
return filteredTags, nil
|
||
|
}
|
||
|
|
||
|
log.Info().Str("regex", regex).Msg("filtering tags using regex")
|
||
|
|
||
|
tagReg, err := regexp.Compile(regex)
|
||
|
if err != nil {
|
||
|
log.Error().Err(err).Str("regex", regex).Msg("couldn't compile regex")
|
||
|
|
||
|
return filteredTags, err
|
||
|
}
|
||
|
|
||
|
for _, tag := range tags {
|
||
|
if tagReg.MatchString(tag) {
|
||
|
filteredTags = append(filteredTags, tag)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return filteredTags, nil
|
||
|
}
|
||
|
|
||
|
// filterTagsBySemver filters tags by checking if they are semver compliant.
|
||
|
func filterTagsBySemver(tags []string, log log.Logger) []string {
|
||
|
filteredTags := []string{}
|
||
|
|
||
|
log.Info().Msg("start filtering using semver compliant rule")
|
||
|
|
||
|
for _, tag := range tags {
|
||
|
_, err := semver.NewVersion(tag)
|
||
|
if err == nil {
|
||
|
filteredTags = append(filteredTags, tag)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return filteredTags
|
||
|
}
|