mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
ce4924f841
Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
277 lines
7.4 KiB
Go
277 lines
7.4 KiB
Go
package retention
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
glob "github.com/bmatcuk/doublestar/v4"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
|
zerr "zotregistry.dev/zot/errors"
|
|
"zotregistry.dev/zot/pkg/api/config"
|
|
zcommon "zotregistry.dev/zot/pkg/common"
|
|
zlog "zotregistry.dev/zot/pkg/log"
|
|
mTypes "zotregistry.dev/zot/pkg/meta/types"
|
|
"zotregistry.dev/zot/pkg/retention/types"
|
|
)
|
|
|
|
const (
|
|
// reasons for gc.
|
|
filteredByTagRules = "didn't meet any tag retention rule"
|
|
filteredByTagNames = "didn't meet any tag 'patterns' rules"
|
|
// reasons for retention.
|
|
retainedStrFormat = "retained by %s policy"
|
|
)
|
|
|
|
type candidatesRules struct {
|
|
candidates []*types.Candidate
|
|
// tag retention rules
|
|
rules []types.Rule
|
|
}
|
|
|
|
type policyManager struct {
|
|
config config.ImageRetention
|
|
regex *RegexMatcher
|
|
log zlog.Logger
|
|
auditLog *zlog.Logger
|
|
}
|
|
|
|
func NewPolicyManager(config config.ImageRetention, log zlog.Logger, auditLog *zlog.Logger) policyManager {
|
|
return policyManager{
|
|
config: config,
|
|
regex: NewRegexMatcher(),
|
|
log: log,
|
|
auditLog: auditLog,
|
|
}
|
|
}
|
|
|
|
func (p policyManager) HasDeleteUntagged(repo string) bool {
|
|
if policy, err := p.getRepoPolicy(repo); err == nil {
|
|
if policy.DeleteUntagged != nil {
|
|
return *policy.DeleteUntagged
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// default
|
|
return false
|
|
}
|
|
|
|
func (p policyManager) HasDeleteReferrer(repo string) bool {
|
|
if policy, err := p.getRepoPolicy(repo); err == nil {
|
|
return policy.DeleteReferrers
|
|
}
|
|
|
|
// default
|
|
return false
|
|
}
|
|
|
|
func (p policyManager) HasTagRetention(repo string) bool {
|
|
if policy, err := p.getRepoPolicy(repo); err == nil {
|
|
return len(policy.KeepTags) > 0
|
|
}
|
|
|
|
// default
|
|
return false
|
|
}
|
|
|
|
func (p policyManager) getRules(tagPolicy config.KeepTagsPolicy) []types.Rule {
|
|
rules := make([]types.Rule, 0)
|
|
|
|
if tagPolicy.MostRecentlyPulledCount != 0 {
|
|
rules = append(rules, NewLatestPull(tagPolicy.MostRecentlyPulledCount))
|
|
}
|
|
|
|
if tagPolicy.MostRecentlyPushedCount != 0 {
|
|
rules = append(rules, NewLatestPush(tagPolicy.MostRecentlyPushedCount))
|
|
}
|
|
|
|
if tagPolicy.PulledWithin != nil {
|
|
rules = append(rules, NewDaysPull(*tagPolicy.PulledWithin))
|
|
}
|
|
|
|
if tagPolicy.PushedWithin != nil {
|
|
rules = append(rules, NewDaysPush(*tagPolicy.PushedWithin))
|
|
}
|
|
|
|
return rules
|
|
}
|
|
|
|
func (p policyManager) GetRetainedTags(ctx context.Context, repoMeta mTypes.RepoMeta, index ispec.Index) []string {
|
|
repo := repoMeta.Name
|
|
|
|
matchedByName := make([]string, 0)
|
|
|
|
candidates := GetCandidates(repoMeta)
|
|
retainTags := make([]string, 0)
|
|
|
|
// we need to make sure tags for which we can not find statistics in repoDB are not removed
|
|
actualTags := getIndexTags(index)
|
|
|
|
// find tags which are not in candidates list, if they are not in repoDB we want to keep them
|
|
for _, tag := range actualTags {
|
|
found := false
|
|
|
|
for _, candidate := range candidates {
|
|
if candidate.Tag == tag {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
p.log.Info().Str("module", "retention").
|
|
Bool("dry-run", p.config.DryRun).
|
|
Str("repository", repo).
|
|
Str("tag", tag).
|
|
Str("decision", "keep").
|
|
Str("reason", "tag statistics not found").Msg("will keep tag")
|
|
|
|
retainTags = append(retainTags, tag)
|
|
}
|
|
}
|
|
|
|
// group all tags by tag policy
|
|
grouped := p.groupCandidatesByTagPolicy(repo, candidates)
|
|
|
|
for _, candidates := range grouped {
|
|
if zcommon.IsContextDone(ctx) {
|
|
return nil
|
|
}
|
|
|
|
retainCandidates := candidates.candidates // copy
|
|
// tag rules
|
|
rules := candidates.rules
|
|
|
|
for _, retainedByName := range retainCandidates {
|
|
matchedByName = append(matchedByName, retainedByName.Tag)
|
|
}
|
|
|
|
rulesCandidates := make([]*types.Candidate, 0)
|
|
|
|
// we retain candidates if any of the below rules are met (OR logic between rules)
|
|
for _, rule := range rules {
|
|
ruleCandidates := rule.Perform(retainCandidates)
|
|
|
|
rulesCandidates = append(rulesCandidates, ruleCandidates...)
|
|
}
|
|
|
|
// if we applied any rule
|
|
if len(rules) > 0 {
|
|
retainCandidates = rulesCandidates
|
|
} // else we retain just the one matching name rule
|
|
|
|
for _, retainCandidate := range retainCandidates {
|
|
// there may be duplicates
|
|
if !zcommon.Contains(retainTags, retainCandidate.Tag) {
|
|
// format reason log msg
|
|
reason := fmt.Sprintf(retainedStrFormat, retainCandidate.RetainedBy)
|
|
|
|
logAction(repo, "keep", reason, retainCandidate, p.config.DryRun, &p.log)
|
|
|
|
retainTags = append(retainTags, retainCandidate.Tag)
|
|
}
|
|
}
|
|
}
|
|
|
|
// log tags which will be removed
|
|
for _, candidateInfo := range candidates {
|
|
if !zcommon.Contains(retainTags, candidateInfo.Tag) {
|
|
var reason string
|
|
if zcommon.Contains(matchedByName, candidateInfo.Tag) {
|
|
reason = filteredByTagRules
|
|
} else {
|
|
reason = filteredByTagNames
|
|
}
|
|
|
|
logAction(repo, "delete", reason, candidateInfo, p.config.DryRun, &p.log)
|
|
|
|
if p.auditLog != nil {
|
|
logAction(repo, "delete", reason, candidateInfo, p.config.DryRun, p.auditLog)
|
|
}
|
|
}
|
|
}
|
|
|
|
return retainTags
|
|
}
|
|
|
|
func (p policyManager) getRepoPolicy(repo string) (config.RetentionPolicy, error) {
|
|
for _, policy := range p.config.Policies {
|
|
for _, pattern := range policy.Repositories {
|
|
matched, err := glob.Match(pattern, repo)
|
|
if err == nil && matched {
|
|
return policy, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return config.RetentionPolicy{}, zerr.ErrRetentionPolicyNotFound
|
|
}
|
|
|
|
func (p policyManager) getTagPolicy(tag string, tagPolicies []config.KeepTagsPolicy,
|
|
) (config.KeepTagsPolicy, int, error) {
|
|
for idx, tagPolicy := range tagPolicies {
|
|
if p.regex.MatchesListOfRegex(tag, tagPolicy.Patterns) {
|
|
return tagPolicy, idx, nil
|
|
}
|
|
}
|
|
|
|
return config.KeepTagsPolicy{}, -1, zerr.ErrRetentionPolicyNotFound
|
|
}
|
|
|
|
// groups candidates by tag policies, tags which don't match any policy are automatically excluded from this map.
|
|
func (p policyManager) groupCandidatesByTagPolicy(repo string, candidates []*types.Candidate,
|
|
) map[int]candidatesRules {
|
|
candidatesByTagPolicy := make(map[int]candidatesRules)
|
|
|
|
// no need to check for error, at this point we have both repo policy for this repo and non nil tags policy
|
|
repoPolicy, _ := p.getRepoPolicy(repo)
|
|
|
|
for _, candidateInfo := range candidates {
|
|
tagPolicy, tagPolicyID, err := p.getTagPolicy(candidateInfo.Tag, repoPolicy.KeepTags)
|
|
if err != nil {
|
|
// no tag policy found for the current candidate, skip it (will be gc'ed)
|
|
continue
|
|
}
|
|
|
|
candidateInfo.RetainedBy = "patterns"
|
|
|
|
if _, ok := candidatesByTagPolicy[tagPolicyID]; !ok {
|
|
candidatesRules := candidatesRules{candidates: []*types.Candidate{candidateInfo}}
|
|
candidatesRules.rules = p.getRules(tagPolicy)
|
|
candidatesByTagPolicy[tagPolicyID] = candidatesRules
|
|
} else {
|
|
candidatesRules := candidatesByTagPolicy[tagPolicyID]
|
|
candidatesRules.candidates = append(candidatesRules.candidates, candidateInfo)
|
|
candidatesByTagPolicy[tagPolicyID] = candidatesRules
|
|
}
|
|
}
|
|
|
|
return candidatesByTagPolicy
|
|
}
|
|
|
|
func logAction(repo, decision, reason string, candidate *types.Candidate, dryRun bool, log *zlog.Logger) {
|
|
log.Info().Str("module", "retention").
|
|
Bool("dry-run", dryRun).
|
|
Str("repository", repo).
|
|
Str("mediaType", candidate.MediaType).
|
|
Str("digest", candidate.DigestStr).
|
|
Str("tag", candidate.Tag).
|
|
Str("lastPullTimestamp", candidate.PullTimestamp.String()).
|
|
Str("pushTimestamp", candidate.PushTimestamp.String()).
|
|
Str("decision", decision).
|
|
Str("reason", reason).Msg("applied policy")
|
|
}
|
|
|
|
func getIndexTags(index ispec.Index) []string {
|
|
tags := make([]string, 0)
|
|
|
|
for _, desc := range index.Manifests {
|
|
tag, ok := desc.Annotations[ispec.AnnotationRefName]
|
|
if ok {
|
|
tags = append(tags, tag)
|
|
}
|
|
}
|
|
|
|
return tags
|
|
}
|