2023-11-01 18:16:18 +02:00
package retention
import (
2023-11-24 10:40:10 +02:00
"context"
2023-11-01 18:16:18 +02:00
"fmt"
glob "github.com/bmatcuk/doublestar/v4"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
2024-02-01 06:34:07 +02:00
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"
2023-11-01 18:16:18 +02:00
)
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
}
2023-11-24 10:40:10 +02:00
func ( p policyManager ) GetRetainedTags ( ctx context . Context , repoMeta mTypes . RepoMeta , index ispec . Index ) [ ] string {
2023-11-01 18:16:18 +02:00
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 {
2023-11-24 10:40:10 +02:00
if zcommon . IsContextDone ( ctx ) {
return nil
}
2023-11-01 18:16:18 +02:00
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
}