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 }