diff --git a/src/core/api/immutabletagrule.go b/src/core/api/immutabletagrule.go index 7f5eaa40a..09163d641 100644 --- a/src/core/api/immutabletagrule.go +++ b/src/core/api/immutabletagrule.go @@ -46,7 +46,7 @@ func (itr *ImmutableTagRuleAPI) Prepare() { itr.ID = ruleID } - itr.ctr = immutabletag.NewAPIController(immutabletag.NewDefaultRuleManager()) + itr.ctr = immutabletag.ImmuCtr if strings.EqualFold(itr.Ctx.Request.Method, "get") { if !itr.requireAccess(rbac.ActionList) { diff --git a/src/pkg/immutabletag/controller.go b/src/pkg/immutabletag/controller.go index 63335eb12..482286d92 100644 --- a/src/pkg/immutabletag/controller.go +++ b/src/pkg/immutabletag/controller.go @@ -4,6 +4,11 @@ import ( "github.com/goharbor/harbor/src/pkg/immutabletag/model" ) +var ( + // ImmuCtr is a global variable for the default immutable controller implementation + ImmuCtr = NewAPIController(NewDefaultRuleManager()) +) + // APIController to handle the requests related with immutabletag type APIController interface { // GetImmutableRule ... diff --git a/src/pkg/immutabletag/match/matcher.go b/src/pkg/immutabletag/match/matcher.go new file mode 100644 index 000000000..73c60cfb5 --- /dev/null +++ b/src/pkg/immutabletag/match/matcher.go @@ -0,0 +1,11 @@ +package match + +import ( + "github.com/goharbor/harbor/src/pkg/art" +) + +// ImmutableTagMatcher ... +type ImmutableTagMatcher interface { + // Match whether the candidate is in the immutable list + Match(c art.Candidate) (bool, error) +} diff --git a/src/pkg/immutabletag/match/rule/match.go b/src/pkg/immutabletag/match/rule/match.go new file mode 100644 index 000000000..d06605a12 --- /dev/null +++ b/src/pkg/immutabletag/match/rule/match.go @@ -0,0 +1,88 @@ +package rule + +import ( + "github.com/goharbor/harbor/src/pkg/art" + "github.com/goharbor/harbor/src/pkg/art/selectors/index" + "github.com/goharbor/harbor/src/pkg/immutabletag" + "github.com/goharbor/harbor/src/pkg/immutabletag/match" + "github.com/goharbor/harbor/src/pkg/immutabletag/model" +) + +// Matcher ... +type Matcher struct { + pid int64 + rules []model.Metadata +} + +// Match ... +func (rm *Matcher) Match(c art.Candidate) (bool, error) { + if err := rm.getImmutableRules(); err != nil { + return false, err + } + + cands := []*art.Candidate{&c} + for _, r := range rm.rules { + if r.Disabled { + continue + } + + // match repositories according to the repository selectors + var repositoryCandidates []*art.Candidate + repositorySelectors := r.ScopeSelectors["repository"] + if len(repositorySelectors) < 1 { + continue + } + repositorySelector := repositorySelectors[0] + selector, err := index.Get(repositorySelector.Kind, repositorySelector.Decoration, + repositorySelector.Pattern) + if err != nil { + return false, err + } + repositoryCandidates, err = selector.Select(cands) + if err != nil { + return false, err + } + if len(repositoryCandidates) == 0 { + continue + } + + // match tag according to the tag selectors + var tagCandidates []*art.Candidate + tagSelectors := r.TagSelectors + if len(tagSelectors) < 0 { + continue + } + tagSelector := r.TagSelectors[0] + selector, err = index.Get(tagSelector.Kind, tagSelector.Decoration, + tagSelector.Pattern) + if err != nil { + return false, err + } + tagCandidates, err = selector.Select(cands) + if err != nil { + return false, err + } + if len(tagCandidates) == 0 { + continue + } + + return true, nil + } + return false, nil +} + +func (rm *Matcher) getImmutableRules() error { + rules, err := immutabletag.ImmuCtr.ListImmutableRules(rm.pid) + if err != nil { + return err + } + rm.rules = rules + return nil +} + +// NewRuleMatcher ... +func NewRuleMatcher(pid int64) match.ImmutableTagMatcher { + return &Matcher{ + pid: pid, + } +} diff --git a/src/pkg/immutabletag/match/rule/match_test.go b/src/pkg/immutabletag/match/rule/match_test.go new file mode 100644 index 000000000..05992e3fe --- /dev/null +++ b/src/pkg/immutabletag/match/rule/match_test.go @@ -0,0 +1,162 @@ +package rule + +import ( + "github.com/goharbor/harbor/src/common/utils/test" + "github.com/goharbor/harbor/src/pkg/art" + "github.com/goharbor/harbor/src/pkg/immutabletag" + "github.com/goharbor/harbor/src/pkg/immutabletag/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "testing" + "time" +) + +// MatchTestSuite ... +type MatchTestSuite struct { + suite.Suite + t *testing.T + assert *assert.Assertions + require *require.Assertions + ctr immutabletag.APIController + ruleID int64 + ruleID2 int64 +} + +// SetupSuite ... +func (s *MatchTestSuite) SetupSuite() { + test.InitDatabaseFromEnv() + s.t = s.T() + s.assert = assert.New(s.t) + s.require = require.New(s.t) + s.ctr = immutabletag.ImmuCtr +} + +func (s *MatchTestSuite) TestImmuMatch() { + rule := &model.Metadata{ + ID: 1, + ProjectID: 2, + Priority: 1, + Template: "latestPushedK", + Action: "immuablity", + TagSelectors: []*model.Selector{ + { + Kind: "doublestar", + Decoration: "matches", + Pattern: "release-[\\d\\.]+", + }, + }, + ScopeSelectors: map[string][]*model.Selector{ + "repository": { + { + Kind: "doublestar", + Decoration: "matches", + Pattern: "redis", + }, + }, + }, + } + + rule2 := &model.Metadata{ + ID: 1, + ProjectID: 2, + Priority: 1, + Template: "latestPushedK", + Action: "immuablity", + TagSelectors: []*model.Selector{ + { + Kind: "doublestar", + Decoration: "matches", + Pattern: "**", + }, + }, + ScopeSelectors: map[string][]*model.Selector{ + "repository": { + { + Kind: "doublestar", + Decoration: "matches", + Pattern: "mysql", + }, + }, + }, + } + + id, err := s.ctr.CreateImmutableRule(rule) + s.ruleID = id + s.require.NotNil(err) + + id, err = s.ctr.CreateImmutableRule(rule2) + s.ruleID2 = id + s.require.NotNil(err) + + match := NewRuleMatcher(2) + + c1 := art.Candidate{ + NamespaceID: 2, + Namespace: "immutable", + Repository: "redis", + Tag: "release-1.10", + Kind: art.Image, + PushedTime: time.Now().Unix() - 3600, + PulledTime: time.Now().Unix(), + CreationTime: time.Now().Unix() - 7200, + Labels: []string{"label1", "label4", "label5"}, + } + isMatch, err := match.Match(c1) + s.require.Equal(isMatch, true) + s.require.Nil(err) + + c2 := art.Candidate{ + NamespaceID: 2, + Namespace: "immutable", + Repository: "redis", + Tag: "1.10", + Kind: art.Image, + PushedTime: time.Now().Unix() - 3600, + PulledTime: time.Now().Unix(), + CreationTime: time.Now().Unix() - 7200, + Labels: []string{"label1", "label4", "label5"}, + } + isMatch, err = match.Match(c2) + s.require.Equal(isMatch, false) + s.require.Nil(err) + + c3 := art.Candidate{ + NamespaceID: 2, + Namespace: "immutable", + Repository: "mysql", + Tag: "9.4.8", + Kind: art.Image, + PushedTime: time.Now().Unix() - 3600, + PulledTime: time.Now().Unix(), + CreationTime: time.Now().Unix() - 7200, + Labels: []string{"label1"}, + } + isMatch, err = match.Match(c3) + s.require.Equal(isMatch, true) + s.require.Nil(err) + + c4 := art.Candidate{ + NamespaceID: 2, + Namespace: "immutable", + Repository: "hello", + Tag: "world", + Kind: art.Image, + PushedTime: time.Now().Unix() - 3600, + PulledTime: time.Now().Unix(), + CreationTime: time.Now().Unix() - 7200, + Labels: []string{"label1"}, + } + isMatch, err = match.Match(c4) + s.require.Equal(isMatch, false) + s.require.Nil(err) +} + +// TearDownSuite clears env for test suite +func (s *MatchTestSuite) TearDownSuite() { + err := s.ctr.DeleteImmutableRule(s.ruleID) + require.NoError(s.T(), err, "delete immutable") + + err = s.ctr.DeleteImmutableRule(s.ruleID2) + require.NoError(s.T(), err, "delete immutable") +}