zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/extensions/sync/content.go (about)

     1  //go:build sync
     2  // +build sync
     3  
     4  package sync
     5  
     6  import (
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/Masterminds/semver"
    11  	glob "github.com/bmatcuk/doublestar/v4"
    12  
    13  	"zotregistry.dev/zot/pkg/common"
    14  	syncconf "zotregistry.dev/zot/pkg/extensions/config/sync"
    15  	"zotregistry.dev/zot/pkg/log"
    16  )
    17  
    18  /* ContentManager uses registry content configuration to filter repos/tags
    19  and also manages applying destination/stripPrefix rules
    20  eg: "content": [
    21  	{
    22  		"prefix": "/repo1/repo",
    23  		"destination": "/repo",
    24  		"stripPrefix": true
    25  		"tags": {
    26  			"regex": "4.*",
    27  			"semver": true
    28  		}
    29  	}
    30  ]
    31  */
    32  
    33  type ContentManager struct {
    34  	contents []syncconf.Content
    35  	log      log.Logger
    36  }
    37  
    38  func NewContentManager(contents []syncconf.Content, log log.Logger) ContentManager {
    39  	return ContentManager{contents: contents, log: log}
    40  }
    41  
    42  /*
    43  MatchesContent returns whether a repo matches a registry
    44  config content (is not filtered out by content config rules).
    45  */
    46  func (cm ContentManager) MatchesContent(repo string) bool {
    47  	content := cm.getContentByUpstreamRepo(repo)
    48  
    49  	return content != nil
    50  }
    51  
    52  // FilterTags filters a repo tags based on content config rules (semver, regex).
    53  func (cm ContentManager) FilterTags(repo string, tags []string) ([]string, error) {
    54  	content := cm.getContentByLocalRepo(repo)
    55  
    56  	var err error
    57  	// filter based on tags rules
    58  	if content != nil && content.Tags != nil {
    59  		if content.Tags.Regex != nil {
    60  			tags, err = filterTagsByRegex(tags, *content.Tags.Regex, cm.log)
    61  			if err != nil {
    62  				return []string{}, err
    63  			}
    64  		}
    65  
    66  		if content.Tags.Semver != nil && *content.Tags.Semver {
    67  			tags = filterTagsBySemver(tags, cm.log)
    68  		}
    69  	}
    70  
    71  	return tags, nil
    72  }
    73  
    74  /*
    75  GetRepoDestination applies content destination config rule and returns the final repo namespace.
    76  - used by periodically sync.
    77  */
    78  func (cm ContentManager) GetRepoDestination(repo string) string {
    79  	content := cm.getContentByUpstreamRepo(repo)
    80  	if content == nil {
    81  		return ""
    82  	}
    83  
    84  	return getRepoDestination(repo, *content)
    85  }
    86  
    87  /*
    88  GetRepoSource is the inverse function of GetRepoDestination, needed in on demand to find out
    89  the remote name of a repo given a local repo.
    90  - used by on demand sync.
    91  */
    92  func (cm ContentManager) GetRepoSource(repo string) string {
    93  	content := cm.getContentByLocalRepo(repo)
    94  	if content == nil {
    95  		return ""
    96  	}
    97  
    98  	return getRepoSource(repo, *content)
    99  }
   100  
   101  // utilies functions.
   102  func (cm ContentManager) getContentByUpstreamRepo(repo string) *syncconf.Content {
   103  	for _, content := range cm.contents {
   104  		var prefix string
   105  		// handle prefixes starting with '/'
   106  		if strings.HasPrefix(content.Prefix, "/") {
   107  			prefix = content.Prefix[1:]
   108  		} else {
   109  			prefix = content.Prefix
   110  		}
   111  
   112  		matched, err := glob.Match(prefix, repo)
   113  		if err != nil {
   114  			cm.log.Error().Str("errorType", common.TypeOf(err)).
   115  				Err(err).Str("pattern",
   116  				prefix).Msg("failed to parse glob pattern, skipping it")
   117  
   118  			continue
   119  		}
   120  
   121  		if matched {
   122  			return &content
   123  		}
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  func (cm ContentManager) getContentByLocalRepo(repo string) *syncconf.Content {
   130  	contentID := -1
   131  	repo = strings.Trim(repo, "/")
   132  
   133  	for cID, content := range cm.contents {
   134  		// make sure prefix ends in "/" to extract the meta characters
   135  		prefix := strings.Trim(content.Prefix, "/") + "/"
   136  		destination := strings.Trim(content.Destination, "/")
   137  
   138  		var patternSlice []string
   139  
   140  		if content.StripPrefix {
   141  			_, metaCharacters := glob.SplitPattern(prefix)
   142  			patternSlice = append(patternSlice, destination, metaCharacters)
   143  		} else {
   144  			patternSlice = append(patternSlice, destination, prefix)
   145  		}
   146  
   147  		pattern := strings.Trim(strings.Join(patternSlice, "/"), "/")
   148  
   149  		matched, err := glob.Match(pattern, repo)
   150  		if err != nil {
   151  			continue
   152  		}
   153  
   154  		if matched {
   155  			contentID = cID
   156  
   157  			break
   158  		}
   159  	}
   160  
   161  	if contentID == -1 {
   162  		return nil
   163  	}
   164  
   165  	return &cm.contents[contentID]
   166  }
   167  
   168  func getRepoSource(localRepo string, content syncconf.Content) string {
   169  	localRepo = strings.Trim(localRepo, "/")
   170  	destination := strings.Trim(content.Destination, "/")
   171  	prefix := strings.Trim(content.Prefix, "/*")
   172  
   173  	var localRepoSlice []string
   174  
   175  	localRepo = strings.TrimPrefix(localRepo, destination)
   176  	localRepo = strings.Trim(localRepo, "/")
   177  
   178  	if content.StripPrefix {
   179  		localRepoSlice = append([]string{prefix}, localRepo)
   180  	} else {
   181  		localRepoSlice = []string{localRepo}
   182  	}
   183  
   184  	repoSource := strings.Join(localRepoSlice, "/")
   185  	if repoSource == "/" {
   186  		return repoSource
   187  	}
   188  
   189  	return strings.Trim(repoSource, "/")
   190  }
   191  
   192  // getRepoDestination returns the local storage path of the synced repo based on the specified destination.
   193  func getRepoDestination(remoteRepo string, content syncconf.Content) string {
   194  	remoteRepo = strings.Trim(remoteRepo, "/")
   195  	destination := strings.Trim(content.Destination, "/")
   196  	prefix := strings.Trim(content.Prefix, "/*")
   197  
   198  	var repoDestSlice []string
   199  
   200  	if content.StripPrefix {
   201  		remoteRepo = strings.TrimPrefix(remoteRepo, prefix)
   202  		remoteRepo = strings.Trim(remoteRepo, "/")
   203  		repoDestSlice = append(repoDestSlice, destination, remoteRepo)
   204  	} else {
   205  		repoDestSlice = append(repoDestSlice, destination, remoteRepo)
   206  	}
   207  
   208  	repoDestination := strings.Join(repoDestSlice, "/")
   209  
   210  	if repoDestination == "/" {
   211  		return "/"
   212  	}
   213  
   214  	return strings.Trim(repoDestination, "/")
   215  }
   216  
   217  // filterTagsByRegex filters images by tag regex given in the config.
   218  func filterTagsByRegex(tags []string, regex string, log log.Logger) ([]string, error) {
   219  	filteredTags := []string{}
   220  
   221  	if len(tags) == 0 || regex == "" {
   222  		return filteredTags, nil
   223  	}
   224  
   225  	log.Info().Str("regex", regex).Msg("filtering tags using regex")
   226  
   227  	tagReg, err := regexp.Compile(regex)
   228  	if err != nil {
   229  		log.Error().Err(err).Str("regex", regex).Msg("failed to compile regex")
   230  
   231  		return filteredTags, err
   232  	}
   233  
   234  	for _, tag := range tags {
   235  		if tagReg.MatchString(tag) {
   236  			filteredTags = append(filteredTags, tag)
   237  		}
   238  	}
   239  
   240  	return filteredTags, nil
   241  }
   242  
   243  // filterTagsBySemver filters tags by checking if they are semver compliant.
   244  func filterTagsBySemver(tags []string, log log.Logger) []string {
   245  	filteredTags := []string{}
   246  
   247  	log.Info().Msg("start filtering using semver compliant rule")
   248  
   249  	for _, tag := range tags {
   250  		_, err := semver.NewVersion(tag)
   251  		if err == nil {
   252  			filteredTags = append(filteredTags, tag)
   253  		}
   254  	}
   255  
   256  	return filteredTags
   257  }