mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 10:45:45 +01:00
feature(tag_retention) add checkbox for user to control whether remove untagged image
Signed-off-by: Ziming Zhang <zziming@vmware.com>
This commit is contained in:
parent
1d8389ab41
commit
8ffa79801b
@ -27,4 +27,4 @@ type Selector interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Factory is factory method to return a selector implementation
|
// Factory is factory method to return a selector implementation
|
||||||
type Factory func(decoration string, pattern string) Selector
|
type Factory func(decoration string, pattern string, extras string) Selector
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
package doublestar
|
package doublestar
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/bmatcuk/doublestar"
|
"github.com/bmatcuk/doublestar"
|
||||||
iselector "github.com/goharbor/harbor/src/internal/selector"
|
iselector "github.com/goharbor/harbor/src/internal/selector"
|
||||||
)
|
)
|
||||||
@ -43,6 +44,8 @@ type selector struct {
|
|||||||
decoration string
|
decoration string
|
||||||
// The pattern expression
|
// The pattern expression
|
||||||
pattern string
|
pattern string
|
||||||
|
// whether match untagged
|
||||||
|
untagged bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select candidates by regular expressions
|
// Select candidates by regular expressions
|
||||||
@ -97,36 +100,55 @@ func (s *selector) Select(artifacts []*iselector.Candidate) (selected []*iselect
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *selector) tagSelectMatch(artifact *iselector.Candidate) (selected bool, err error) {
|
func (s *selector) tagSelectMatch(artifact *iselector.Candidate) (selected bool, err error) {
|
||||||
for _, t := range artifact.Tags {
|
if len(artifact.Tags) > 0 {
|
||||||
matched, err := match(s.pattern, t)
|
for _, t := range artifact.Tags {
|
||||||
if err != nil {
|
matched, err := match(s.pattern, t)
|
||||||
return false, err
|
if err != nil {
|
||||||
}
|
return false, err
|
||||||
if matched {
|
}
|
||||||
return true, nil
|
if matched {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, nil
|
return s.untagged, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *selector) tagSelectExclude(artifact *iselector.Candidate) (selected bool, err error) {
|
func (s *selector) tagSelectExclude(artifact *iselector.Candidate) (selected bool, err error) {
|
||||||
for _, t := range artifact.Tags {
|
if len(artifact.Tags) > 0 {
|
||||||
matched, err := match(s.pattern, t)
|
for _, t := range artifact.Tags {
|
||||||
if err != nil {
|
matched, err := match(s.pattern, t)
|
||||||
return false, err
|
if err != nil {
|
||||||
}
|
return false, err
|
||||||
if !matched {
|
}
|
||||||
return true, nil
|
if !matched {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
return false, nil
|
return !s.untagged, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// New is factory method for doublestar selector
|
// New is factory method for doublestar selector
|
||||||
func New(decoration string, pattern string) iselector.Selector {
|
func New(decoration string, pattern string, extras string) iselector.Selector {
|
||||||
|
untagged := true // default behavior for upgrade, active keep the untagged images
|
||||||
|
if decoration == Excludes {
|
||||||
|
untagged = false
|
||||||
|
}
|
||||||
|
if extras != "" {
|
||||||
|
var extraObj struct {
|
||||||
|
Untagged bool `json:"untagged"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(extras), &extraObj); err == nil {
|
||||||
|
untagged = extraObj.Untagged
|
||||||
|
}
|
||||||
|
}
|
||||||
return &selector{
|
return &selector{
|
||||||
decoration: decoration,
|
decoration: decoration,
|
||||||
pattern: pattern,
|
pattern: pattern,
|
||||||
|
untagged: untagged,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,16 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
|
|||||||
CreationTime: time.Now().Unix() - 7200,
|
CreationTime: time.Now().Unix() - 7200,
|
||||||
Labels: []string{"label1", "label4", "label5"},
|
Labels: []string{"label1", "label4", "label5"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
NamespaceID: 3,
|
||||||
|
Namespace: "library",
|
||||||
|
Repository: "special",
|
||||||
|
Tags: nil, // untagged
|
||||||
|
Kind: iselector.Image,
|
||||||
|
PushedTime: time.Now().Unix() - 3600,
|
||||||
|
PulledTime: time.Now().Unix(),
|
||||||
|
CreationTime: time.Now().Unix() - 7200,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,6 +110,15 @@ func (suite *RegExpSelectorTestSuite) TestTagMatches() {
|
|||||||
assert.Condition(suite.T(), func() bool {
|
assert.Condition(suite.T(), func() bool {
|
||||||
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
|
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tagMatches3 := New(Matches, "4.*", "")
|
||||||
|
|
||||||
|
selected, err = tagMatches3.Select(suite.artifacts)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
assert.Equal(suite.T(), 3, len(selected))
|
||||||
|
assert.Condition(suite.T(), func() bool {
|
||||||
|
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTagExcludes tests the tag `excludes` case
|
// TestTagExcludes tests the tag `excludes` case
|
||||||
@ -107,15 +126,23 @@ func (suite *RegExpSelectorTestSuite) TestTagExcludes() {
|
|||||||
tagExcludes := &selector{
|
tagExcludes := &selector{
|
||||||
decoration: Excludes,
|
decoration: Excludes,
|
||||||
pattern: "{latest,4.*}",
|
pattern: "{latest,4.*}",
|
||||||
|
untagged: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
selected, err := tagExcludes.Select(suite.artifacts)
|
selected, err := tagExcludes.Select(suite.artifacts)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), 0, len(selected))
|
assert.Equal(suite.T(), 0, len(selected))
|
||||||
|
|
||||||
|
tagExcludes1 := New(Excludes, "{latest,4.*}", "")
|
||||||
|
|
||||||
|
selected, err = tagExcludes1.Select(suite.artifacts)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
assert.Equal(suite.T(), 1, len(selected))
|
||||||
|
|
||||||
tagExcludes2 := &selector{
|
tagExcludes2 := &selector{
|
||||||
decoration: Excludes,
|
decoration: Excludes,
|
||||||
pattern: "4.*",
|
pattern: "4.*",
|
||||||
|
untagged: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
selected, err = tagExcludes2.Select(suite.artifacts)
|
selected, err = tagExcludes2.Select(suite.artifacts)
|
||||||
@ -162,7 +189,7 @@ func (suite *RegExpSelectorTestSuite) TestRepoExcludes() {
|
|||||||
|
|
||||||
selected, err := repoExcludes.Select(suite.artifacts)
|
selected, err := repoExcludes.Select(suite.artifacts)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), 1, len(selected))
|
assert.Equal(suite.T(), 2, len(selected))
|
||||||
assert.Condition(suite.T(), func() bool {
|
assert.Condition(suite.T(), func() bool {
|
||||||
return expect([]string{"harbor:latest"}, selected)
|
return expect([]string{"harbor:latest"}, selected)
|
||||||
})
|
})
|
||||||
@ -174,7 +201,7 @@ func (suite *RegExpSelectorTestSuite) TestRepoExcludes() {
|
|||||||
|
|
||||||
selected, err = repoExcludes2.Select(suite.artifacts)
|
selected, err = repoExcludes2.Select(suite.artifacts)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), 2, len(selected))
|
assert.Equal(suite.T(), 3, len(selected))
|
||||||
assert.Condition(suite.T(), func() bool {
|
assert.Condition(suite.T(), func() bool {
|
||||||
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
|
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
|
||||||
})
|
})
|
||||||
@ -189,7 +216,7 @@ func (suite *RegExpSelectorTestSuite) TestNSMatches() {
|
|||||||
|
|
||||||
selected, err := repoMatches.Select(suite.artifacts)
|
selected, err := repoMatches.Select(suite.artifacts)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), 1, len(selected))
|
assert.Equal(suite.T(), 2, len(selected))
|
||||||
assert.Condition(suite.T(), func() bool {
|
assert.Condition(suite.T(), func() bool {
|
||||||
return expect([]string{"harbor:latest"}, selected)
|
return expect([]string{"harbor:latest"}, selected)
|
||||||
})
|
})
|
||||||
@ -228,7 +255,7 @@ func (suite *RegExpSelectorTestSuite) TestNSExcludes() {
|
|||||||
|
|
||||||
selected, err = repoExcludes2.Select(suite.artifacts)
|
selected, err = repoExcludes2.Select(suite.artifacts)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), 1, len(selected))
|
assert.Equal(suite.T(), 2, len(selected))
|
||||||
assert.Condition(suite.T(), func() bool {
|
assert.Condition(suite.T(), func() bool {
|
||||||
return expect([]string{"harbor:latest"}, selected)
|
return expect([]string{"harbor:latest"}, selected)
|
||||||
})
|
})
|
||||||
|
@ -69,7 +69,7 @@ func Register(kind string, decorations []string, factory selector.Factory) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get selector with the provided kind and decoration
|
// Get selector with the provided kind and decoration
|
||||||
func Get(kind, decoration, pattern string) (selector.Selector, error) {
|
func Get(kind, decoration, pattern, extras string) (selector.Selector, error) {
|
||||||
if len(kind) == 0 || len(decoration) == 0 {
|
if len(kind) == 0 || len(decoration) == 0 {
|
||||||
return nil, errors.New("empty selector kind or decoration")
|
return nil, errors.New("empty selector kind or decoration")
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ func Get(kind, decoration, pattern string) (selector.Selector, error) {
|
|||||||
for _, dec := range item.Meta.Decorations {
|
for _, dec := range item.Meta.Decorations {
|
||||||
if dec == decoration {
|
if dec == decoration {
|
||||||
factory := item.Factory
|
factory := item.Factory
|
||||||
return factory(decoration, pattern), nil
|
return factory(decoration, pattern, extras), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ func (s *selector) Select(artifacts []*iselector.Candidate) (selected []*iselect
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New is factory method for list selector
|
// New is factory method for list selector
|
||||||
func New(decoration string, pattern string) iselector.Selector {
|
func New(decoration string, pattern string, extras string) iselector.Selector {
|
||||||
labels := make([]string, 0)
|
labels := make([]string, 0)
|
||||||
if len(pattern) > 0 {
|
if len(pattern) > 0 {
|
||||||
labels = append(labels, strings.Split(pattern, ",")...)
|
labels = append(labels, strings.Split(pattern, ",")...)
|
||||||
|
@ -33,7 +33,7 @@ func (rm *Matcher) Match(pid int64, c iselector.Candidate) (bool, error) {
|
|||||||
}
|
}
|
||||||
repositorySelector := repositorySelectors[0]
|
repositorySelector := repositorySelectors[0]
|
||||||
selector, err := index.Get(repositorySelector.Kind, repositorySelector.Decoration,
|
selector, err := index.Get(repositorySelector.Kind, repositorySelector.Decoration,
|
||||||
repositorySelector.Pattern)
|
repositorySelector.Pattern, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ func (rm *Matcher) Match(pid int64, c iselector.Candidate) (bool, error) {
|
|||||||
}
|
}
|
||||||
tagSelector := r.TagSelectors[0]
|
tagSelector := r.TagSelectors[0]
|
||||||
selector, err = index.Get(tagSelector.Kind, tagSelector.Decoration,
|
selector, err = index.Get(tagSelector.Kind, tagSelector.Decoration,
|
||||||
tagSelector.Pattern)
|
tagSelector.Pattern, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
|
|||||||
// filter projects according to the project selectors
|
// filter projects according to the project selectors
|
||||||
for _, projectSelector := range rule.ScopeSelectors["project"] {
|
for _, projectSelector := range rule.ScopeSelectors["project"] {
|
||||||
selector, err := index.Get(projectSelector.Kind, projectSelector.Decoration,
|
selector, err := index.Get(projectSelector.Kind, projectSelector.Decoration,
|
||||||
projectSelector.Pattern)
|
projectSelector.Pattern, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, launcherError(err)
|
return 0, launcherError(err)
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ func (l *launcher) Launch(ply *policy.Metadata, executionID int64, isDryRun bool
|
|||||||
// filter repositories according to the repository selectors
|
// filter repositories according to the repository selectors
|
||||||
for _, repositorySelector := range rule.ScopeSelectors["repository"] {
|
for _, repositorySelector := range rule.ScopeSelectors["repository"] {
|
||||||
selector, err := index.Get(repositorySelector.Kind, repositorySelector.Decoration,
|
selector, err := index.Get(repositorySelector.Kind, repositorySelector.Decoration,
|
||||||
repositorySelector.Pattern)
|
repositorySelector.Pattern, repositorySelector.Extras)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, launcherError(err)
|
return 0, launcherError(err)
|
||||||
}
|
}
|
||||||
|
@ -93,8 +93,8 @@ func (suite *ProcessorTestSuite) TestProcess() {
|
|||||||
params = append(params, &alg.Parameter{
|
params = append(params, &alg.Parameter{
|
||||||
Evaluator: lastx.New(lastxParams),
|
Evaluator: lastx.New(lastxParams),
|
||||||
Selectors: []selector.Selector{
|
Selectors: []selector.Selector{
|
||||||
doublestar.New(doublestar.Matches, "*dev*"),
|
doublestar.New(doublestar.Matches, "*dev*", ""),
|
||||||
label.New(label.With, "L1,L2"),
|
label.New(label.With, "L1,L2", ""),
|
||||||
},
|
},
|
||||||
Performer: perf,
|
Performer: perf,
|
||||||
})
|
})
|
||||||
@ -104,7 +104,7 @@ func (suite *ProcessorTestSuite) TestProcess() {
|
|||||||
params = append(params, &alg.Parameter{
|
params = append(params, &alg.Parameter{
|
||||||
Evaluator: latestps.New(latestKParams),
|
Evaluator: latestps.New(latestKParams),
|
||||||
Selectors: []selector.Selector{
|
Selectors: []selector.Selector{
|
||||||
label.New(label.With, "L3"),
|
label.New(label.With, "L3", ""),
|
||||||
},
|
},
|
||||||
Performer: perf,
|
Performer: perf,
|
||||||
})
|
})
|
||||||
@ -134,8 +134,8 @@ func (suite *ProcessorTestSuite) TestProcess2() {
|
|||||||
params = append(params, &alg.Parameter{
|
params = append(params, &alg.Parameter{
|
||||||
Evaluator: always.New(alwaysParams),
|
Evaluator: always.New(alwaysParams),
|
||||||
Selectors: []selector.Selector{
|
Selectors: []selector.Selector{
|
||||||
doublestar.New(doublestar.Matches, "latest"),
|
doublestar.New(doublestar.Matches, "latest", ""),
|
||||||
label.New(label.With, ""),
|
label.New(label.With, "", ""),
|
||||||
},
|
},
|
||||||
Performer: perf,
|
Performer: perf,
|
||||||
})
|
})
|
||||||
|
@ -78,7 +78,7 @@ func (bb *basicBuilder) Build(policy *lwp.Metadata, isDryRun bool) (alg.Processo
|
|||||||
|
|
||||||
sl := make([]selector.Selector, 0)
|
sl := make([]selector.Selector, 0)
|
||||||
for _, s := range r.TagSelectors {
|
for _, s := range r.TagSelectors {
|
||||||
sel, err := index2.Get(s.Kind, s.Decoration, s.Pattern)
|
sel, err := index2.Get(s.Kind, s.Decoration, s.Pattern, s.Extras)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "get selector by metadata")
|
return nil, errors.Wrap(err, "get selector by metadata")
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,9 @@ type Selector struct {
|
|||||||
|
|
||||||
// Param for the selector
|
// Param for the selector
|
||||||
Pattern string `json:"pattern" valid:"Required"`
|
Pattern string `json:"pattern" valid:"Required"`
|
||||||
|
|
||||||
|
// Extras for other settings
|
||||||
|
Extras string `json:"extras"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameters of rule, indexed by the key
|
// Parameters of rule, indexed by the key
|
||||||
|
@ -87,7 +87,7 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
{
|
{
|
||||||
linkName: "tag-strategy",
|
linkName: "tag-strategy",
|
||||||
tabLinkInOverflow: false,
|
tabLinkInOverflow: false,
|
||||||
showTabName: "PROJECT_DETAIL.TAG_STRATEGY",
|
showTabName: "PROJECT_DETAIL.POLICY",
|
||||||
permissions: () => this.hasTagRetentionPermission
|
permissions: () => this.hasTagRetentionPermission
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div class="clr-col-4">
|
<div class="clr-col-4">
|
||||||
<span>{{'TAG_RETENTION.IN_REPOSITORIES' | translate}}</span>
|
<span>{{'TAG_RETENTION.IN_REPOSITORIES' | translate}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="clr-col-3">
|
<div class="clr-col-2">
|
||||||
<div class="clr-select-wrapper w-100">
|
<div class="clr-select-wrapper w-100">
|
||||||
<select [(ngModel)]="repoSelect" class="clr-select w-100">
|
<select [(ngModel)]="repoSelect" class="clr-select w-100">
|
||||||
<option *ngFor="let d of metadata?.scope_selectors[0]?.decorations"
|
<option *ngFor="let d of metadata?.scope_selectors[0]?.decorations"
|
||||||
@ -18,7 +18,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="clr-col-5">
|
<div class="clr-col-6">
|
||||||
<div class="w-100">
|
<div class="w-100">
|
||||||
<input id="repos" required [(ngModel)]="repositories" class="clr-input w-100">
|
<input id="repos" required [(ngModel)]="repositories" class="clr-input w-100">
|
||||||
</div>
|
</div>
|
||||||
@ -57,12 +57,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="height-72">
|
<div class="height-85">
|
||||||
<div class="clr-row">
|
<div class="clr-row">
|
||||||
<div class="clr-col-4">
|
<div class="clr-col-4">
|
||||||
<label>{{'TAG_RETENTION.TAGS' | translate}}</label>
|
<label>{{'TAG_RETENTION.TAGS' | translate}}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="clr-col-3">
|
<div class="clr-col-2">
|
||||||
<div class="clr-select-wrapper w-100">
|
<div class="clr-select-wrapper w-100">
|
||||||
<select [(ngModel)]="tagsSelect" class="clr-select w-100">
|
<select [(ngModel)]="tagsSelect" class="clr-select w-100">
|
||||||
<option *ngFor="let d of metadata?.tag_selectors[0]?.decorations"
|
<option *ngFor="let d of metadata?.tag_selectors[0]?.decorations"
|
||||||
@ -70,16 +70,22 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="clr-col-5">
|
<div class="clr-col-3">
|
||||||
<div class="w-100">
|
<div class="w-100">
|
||||||
<input id="tags" required [(ngModel)]="tagsInput" class="clr-input w-100">
|
<input id="tags" required [(ngModel)]="tagsInput" class="clr-input w-100">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="clr-col-3 p-0 font-size-13">
|
||||||
|
<div class="w-100 untagged">
|
||||||
|
<label for="untagged">{{'TAG_RETENTION.INCLUDE_UNTAGGED' | translate}}</label>
|
||||||
|
<input type="checkbox" [(ngModel)]="untagged" name="untagged" id="untagged" class="clr-input w-100" clrCheckbox />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="clr-row">
|
<div class="clr-row">
|
||||||
<div class="clr-col-4"></div>
|
<div class="clr-col-4"></div>
|
||||||
<div class="clr-col-8">
|
<div class="clr-col-8">
|
||||||
<span>{{'TAG_RETENTION.TAG_SEPARATOR' | translate}}</span>
|
<span class="tootip">{{'TAG_RETENTION.TAG_SEPARATOR' | translate}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,25 @@
|
|||||||
.height-72 {
|
.height-72 {
|
||||||
height: 72px;
|
height: 72px;
|
||||||
}
|
}
|
||||||
|
.height-85 {
|
||||||
|
height: 85px;
|
||||||
|
}
|
||||||
|
|
||||||
.display-none {
|
.display-none {
|
||||||
display: none
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.untagged {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
.modal-body {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.tootip {
|
||||||
|
display: block;
|
||||||
|
line-height: .9rem;
|
||||||
|
}
|
||||||
|
.font-size-13 {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
@ -123,6 +123,24 @@ export class AddRuleComponent implements OnInit, OnDestroy {
|
|||||||
get tagsInput() {
|
get tagsInput() {
|
||||||
return this.rule.tag_selectors[0].pattern.replace(/[{}]/g, "");
|
return this.rule.tag_selectors[0].pattern.replace(/[{}]/g, "");
|
||||||
}
|
}
|
||||||
|
set untagged(untagged) {
|
||||||
|
let extras = JSON.parse(this.rule.tag_selectors[0].extras);
|
||||||
|
extras.untagged = untagged;
|
||||||
|
this.rule.tag_selectors[0].extras = JSON.stringify(extras);
|
||||||
|
}
|
||||||
|
|
||||||
|
get untagged() {
|
||||||
|
if (this.rule.tag_selectors[0] && this.rule.tag_selectors[0].extras) {
|
||||||
|
let extras = JSON.parse(this.rule.tag_selectors[0].extras);
|
||||||
|
if (extras.untagged !== undefined) {
|
||||||
|
return extras.untagged;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
get labelsSelect() {
|
get labelsSelect() {
|
||||||
return this.rule.tag_selectors[1].decoration;
|
return this.rule.tag_selectors[1].decoration;
|
||||||
@ -227,6 +245,9 @@ export class AddRuleComponent implements OnInit, OnDestroy {
|
|||||||
if (this.rule.tag_selectors[0].decoration !== rule.tag_selectors[0].decoration) {
|
if (this.rule.tag_selectors[0].decoration !== rule.tag_selectors[0].decoration) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (this.rule.tag_selectors[0].extras !== rule.tag_selectors[0].extras) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return this.rule.tag_selectors[0].pattern === rule.tag_selectors[0].pattern;
|
return this.rule.tag_selectors[0].pattern === rule.tag_selectors[0].pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,7 @@ export class Rule extends BaseRule {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.params = {};
|
this.params = {};
|
||||||
|
this.tag_selectors[0].extras = JSON.stringify({untagged: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +104,7 @@ export class Selector {
|
|||||||
kind: string;
|
kind: string;
|
||||||
decoration: string;
|
decoration: string;
|
||||||
pattern: string;
|
pattern: string;
|
||||||
|
extras?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Param {
|
export class Param {
|
||||||
|
@ -44,6 +44,9 @@
|
|||||||
<span>{{'TAG_RETENTION.LOWER_TAGS' | translate}}</span>
|
<span>{{'TAG_RETENTION.LOWER_TAGS' | translate}}</span>
|
||||||
<span>{{getI18nKey(rule?.tag_selectors[0]?.decoration)|translate}}</span>
|
<span>{{getI18nKey(rule?.tag_selectors[0]?.decoration)|translate}}</span>
|
||||||
<span>{{formatPattern(rule?.tag_selectors[0]?.pattern)}}</span>
|
<span>{{formatPattern(rule?.tag_selectors[0]?.pattern)}}</span>
|
||||||
|
<span class="color-97">{{ showUntagged(rule?.tag_selectors[0]?.extras) ? ('TAG_RETENTION.WITH_CONDITION' | translate) :''}}</span>
|
||||||
|
<span>{{ showUntagged(rule?.tag_selectors[0]?.extras) ? ( 'TAG_RETENTION.UNTAGGED' | translate ) : ''}}</span>
|
||||||
|
|
||||||
<ng-container *ngIf="rule?.tag_selectors[1]?.pattern && rule?.tag_selectors[1]?.pattern">
|
<ng-container *ngIf="rule?.tag_selectors[1]?.pattern && rule?.tag_selectors[1]?.pattern">
|
||||||
<span class="color-97">{{'TAG_RETENTION.AND' | translate}}</span>
|
<span class="color-97">{{'TAG_RETENTION.AND' | translate}}</span>
|
||||||
<span>{{'TAG_RETENTION.LOWER_LABELS' | translate}}</span>
|
<span>{{'TAG_RETENTION.LOWER_LABELS' | translate}}</span>
|
||||||
|
@ -37,6 +37,10 @@ const SCHEDULE_TYPE = {
|
|||||||
HOURLY: "Hourly",
|
HOURLY: "Hourly",
|
||||||
CUSTOM: "Custom"
|
CUSTOM: "Custom"
|
||||||
};
|
};
|
||||||
|
const DECORATION = {
|
||||||
|
MATCHES: "matches",
|
||||||
|
EXCLUDES: "excludes",
|
||||||
|
};
|
||||||
const RUNNING: string = "Running";
|
const RUNNING: string = "Running";
|
||||||
const PENDING: string = "pending";
|
const PENDING: string = "pending";
|
||||||
const TIMEOUT: number = 5000;
|
const TIMEOUT: number = 5000;
|
||||||
@ -176,6 +180,8 @@ export class TagRetentionComponent implements OnInit {
|
|||||||
if (!item.params) {
|
if (!item.params) {
|
||||||
item.params = {};
|
item.params = {};
|
||||||
}
|
}
|
||||||
|
this.setRuleUntagged(item);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.retention = response;
|
this.retention = response;
|
||||||
@ -225,7 +231,28 @@ export class TagRetentionComponent implements OnInit {
|
|||||||
this.errorHandler.error(error);
|
this.errorHandler.error(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setRuleUntagged(rule) {
|
||||||
|
if (!rule.tag_selectors[0].extras) {
|
||||||
|
if (rule.tag_selectors[0].decoration === DECORATION.MATCHES) {
|
||||||
|
rule.tag_selectors[0].extras = JSON.stringify({untagged: true});
|
||||||
|
}
|
||||||
|
if (rule.tag_selectors[0].decoration === DECORATION.EXCLUDES) {
|
||||||
|
rule.tag_selectors[0].extras = JSON.stringify({untagged: false});
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let extras = JSON.parse(rule.tag_selectors[0].extras);
|
||||||
|
if (extras.untagged === undefined) {
|
||||||
|
if (rule.tag_selectors[0].decoration === DECORATION.MATCHES) {
|
||||||
|
extras.untagged = true;
|
||||||
|
}
|
||||||
|
if (rule.tag_selectors[0].decoration === DECORATION.EXCLUDES) {
|
||||||
|
extras.untagged = false;
|
||||||
|
}
|
||||||
|
rule.tag_selectors[0].extras = JSON.stringify(extras);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
openAddRule() {
|
openAddRule() {
|
||||||
this.addRuleComponent.open();
|
this.addRuleComponent.open();
|
||||||
this.addRuleComponent.isAdd = true;
|
this.addRuleComponent.isAdd = true;
|
||||||
@ -450,4 +477,11 @@ export class TagRetentionComponent implements OnInit {
|
|||||||
|
|
||||||
this.refreshList();
|
this.refreshList();
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param extras Json string
|
||||||
|
*/
|
||||||
|
showUntagged(extras) {
|
||||||
|
return JSON.parse(extras).untagged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,7 +251,7 @@
|
|||||||
"ROBOT_ACCOUNTS": "Robot Accounts",
|
"ROBOT_ACCOUNTS": "Robot Accounts",
|
||||||
"WEBHOOKS": "Webhooks",
|
"WEBHOOKS": "Webhooks",
|
||||||
"IMMUTABLE_TAG": "Tag Immutability",
|
"IMMUTABLE_TAG": "Tag Immutability",
|
||||||
"TAG_STRATEGY": "Tag Strategy"
|
"POLICY": "Policy"
|
||||||
},
|
},
|
||||||
"PROJECT_CONFIG": {
|
"PROJECT_CONFIG": {
|
||||||
"REGISTRY": "Project registry",
|
"REGISTRY": "Project registry",
|
||||||
@ -1250,9 +1250,11 @@
|
|||||||
"IN_REPOSITORIES": "For the repositories",
|
"IN_REPOSITORIES": "For the repositories",
|
||||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||||
"TAGS": "Tags",
|
"TAGS": "Tags",
|
||||||
|
"UNTAGGED": " untagged",
|
||||||
|
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||||
"MATCHES_TAGS": "Matches tags",
|
"MATCHES_TAGS": "Matches tags",
|
||||||
"MATCHES_EXCEPT_TAGS": "Matches except tags",
|
"MATCHES_EXCEPT_TAGS": "Matches except tags",
|
||||||
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,or **",
|
"TAG_SEPARATOR": "Enter multiple comma separated tags, tag*, or **. Optionally include all untagged artifacts when applying the ‘including’ or ‘excluding’ selector by checking the box above.",
|
||||||
"LABELS": "Labels",
|
"LABELS": "Labels",
|
||||||
"MATCHES_LABELS": "Matches Labels",
|
"MATCHES_LABELS": "Matches Labels",
|
||||||
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
||||||
@ -1309,7 +1311,7 @@
|
|||||||
"IN_REPOSITORIES": "For the repositories",
|
"IN_REPOSITORIES": "For the repositories",
|
||||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||||
"TAGS": "Tags",
|
"TAGS": "Tags",
|
||||||
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,or **",
|
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,or **.",
|
||||||
"EDIT_TITLE": "Edit Tag Immutability Rule",
|
"EDIT_TITLE": "Edit Tag Immutability Rule",
|
||||||
"EXC": " excluding ",
|
"EXC": " excluding ",
|
||||||
"MAT": " matching ",
|
"MAT": " matching ",
|
||||||
|
@ -252,7 +252,7 @@
|
|||||||
"ROBOT_ACCOUNTS": "Robot Accounts",
|
"ROBOT_ACCOUNTS": "Robot Accounts",
|
||||||
"WEBHOOKS": "Webhooks",
|
"WEBHOOKS": "Webhooks",
|
||||||
"IMMUTABLE_TAG": "Tag Immutability",
|
"IMMUTABLE_TAG": "Tag Immutability",
|
||||||
"TAG_STRATEGY": "Tag Strategy"
|
"POLICY": "Policy"
|
||||||
},
|
},
|
||||||
"PROJECT_CONFIG": {
|
"PROJECT_CONFIG": {
|
||||||
"REGISTRY": "Registro de proyectos",
|
"REGISTRY": "Registro de proyectos",
|
||||||
@ -1247,9 +1247,11 @@
|
|||||||
"IN_REPOSITORIES": "For the repositories",
|
"IN_REPOSITORIES": "For the repositories",
|
||||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||||
"TAGS": "Tags",
|
"TAGS": "Tags",
|
||||||
|
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||||
|
"UNTAGGED": " untagged",
|
||||||
"MATCHES_TAGS": "Matches tags",
|
"MATCHES_TAGS": "Matches tags",
|
||||||
"MATCHES_EXCEPT_TAGS": "Matches except tags",
|
"MATCHES_EXCEPT_TAGS": "Matches except tags",
|
||||||
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,or **",
|
"TAG_SEPARATOR": "Enter multiple comma separated tags, tag*, or **. Optionally include all untagged artifacts when applying the ‘including’ or ‘excluding’ selector by checking the box above.",
|
||||||
"LABELS": "Labels",
|
"LABELS": "Labels",
|
||||||
"MATCHES_LABELS": "Matches Labels",
|
"MATCHES_LABELS": "Matches Labels",
|
||||||
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
||||||
|
@ -245,7 +245,7 @@
|
|||||||
"ROBOT_ACCOUNTS": "Robot Accounts",
|
"ROBOT_ACCOUNTS": "Robot Accounts",
|
||||||
"WEBHOOKS": "Webhooks",
|
"WEBHOOKS": "Webhooks",
|
||||||
"IMMUTABLE_TAG": "Tag Immutability",
|
"IMMUTABLE_TAG": "Tag Immutability",
|
||||||
"TAG_STRATEGY": "Tag Strategy"
|
"POLICY": "Policy"
|
||||||
},
|
},
|
||||||
"PROJECT_CONFIG": {
|
"PROJECT_CONFIG": {
|
||||||
"REGISTRY": "Dépôt du Projet",
|
"REGISTRY": "Dépôt du Projet",
|
||||||
@ -1217,9 +1217,11 @@
|
|||||||
"IN_REPOSITORIES": "For the repositories",
|
"IN_REPOSITORIES": "For the repositories",
|
||||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||||
"TAGS": "Tags",
|
"TAGS": "Tags",
|
||||||
|
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||||
|
"UNTAGGED": " untagged",
|
||||||
"MATCHES_TAGS": "Matches tags",
|
"MATCHES_TAGS": "Matches tags",
|
||||||
"MATCHES_EXCEPT_TAGS": "Matches except tags",
|
"MATCHES_EXCEPT_TAGS": "Matches except tags",
|
||||||
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,or **",
|
"TAG_SEPARATOR": "Enter multiple comma separated tags, tag*, or **. Optionally include all untagged artifacts when applying the ‘including’ or ‘excluding’ selector by checking the box above.",
|
||||||
"LABELS": "Labels",
|
"LABELS": "Labels",
|
||||||
"MATCHES_LABELS": "Matches Labels",
|
"MATCHES_LABELS": "Matches Labels",
|
||||||
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
||||||
|
@ -249,7 +249,7 @@
|
|||||||
"ROBOT_ACCOUNTS": "Robot Accounts",
|
"ROBOT_ACCOUNTS": "Robot Accounts",
|
||||||
"WEBHOOKS": "Webhooks",
|
"WEBHOOKS": "Webhooks",
|
||||||
"IMMUTABLE_TAG": "Tag Immutability",
|
"IMMUTABLE_TAG": "Tag Immutability",
|
||||||
"TAG_STRATEGY": "Tag Strategy"
|
"POLICY": "Policy"
|
||||||
},
|
},
|
||||||
"PROJECT_CONFIG": {
|
"PROJECT_CONFIG": {
|
||||||
"REGISTRY": "Registro do Projeto",
|
"REGISTRY": "Registro do Projeto",
|
||||||
@ -1245,9 +1245,11 @@
|
|||||||
"IN_REPOSITORIES": "For the repositories",
|
"IN_REPOSITORIES": "For the repositories",
|
||||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||||
"TAGS": "Tags",
|
"TAGS": "Tags",
|
||||||
|
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||||
|
"UNTAGGED": " untagged",
|
||||||
"MATCHES_TAGS": "Matches tags",
|
"MATCHES_TAGS": "Matches tags",
|
||||||
"MATCHES_EXCEPT_TAGS": "Matches except tags",
|
"MATCHES_EXCEPT_TAGS": "Matches except tags",
|
||||||
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,or **",
|
"TAG_SEPARATOR": "Enter multiple comma separated tags, tag*, or **. Optionally include all untagged artifacts when applying the ‘including’ or ‘excluding’ selector by checking the box above.",
|
||||||
"LABELS": "Labels",
|
"LABELS": "Labels",
|
||||||
"MATCHES_LABELS": "Matches Labels",
|
"MATCHES_LABELS": "Matches Labels",
|
||||||
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
||||||
|
@ -251,7 +251,7 @@
|
|||||||
"ROBOT_ACCOUNTS": "Robot Hesapları",
|
"ROBOT_ACCOUNTS": "Robot Hesapları",
|
||||||
"WEBHOOKS": "Ağ Kancaları",
|
"WEBHOOKS": "Ağ Kancaları",
|
||||||
"IMMUTABLE_TAG": "Tag Immutability",
|
"IMMUTABLE_TAG": "Tag Immutability",
|
||||||
"TAG_STRATEGY": "Tag Strategy"
|
"POLICY": "Policy"
|
||||||
},
|
},
|
||||||
"PROJECT_CONFIG": {
|
"PROJECT_CONFIG": {
|
||||||
"REGISTRY": "Proje kaydı",
|
"REGISTRY": "Proje kaydı",
|
||||||
@ -1249,9 +1249,11 @@
|
|||||||
"IN_REPOSITORIES": "Depolar için",
|
"IN_REPOSITORIES": "Depolar için",
|
||||||
"REP_SEPARATOR": "Birden çok virgülle ayrılmış depolar, depo* veya ** girin",
|
"REP_SEPARATOR": "Birden çok virgülle ayrılmış depolar, depo* veya ** girin",
|
||||||
"TAGS": "Etiketler",
|
"TAGS": "Etiketler",
|
||||||
|
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||||
|
"UNTAGGED": " untagged",
|
||||||
"MATCHES_TAGS": "Etiketleri eşleşir",
|
"MATCHES_TAGS": "Etiketleri eşleşir",
|
||||||
"MATCHES_EXCEPT_TAGS": "Etiketler hariç eşleşir",
|
"MATCHES_EXCEPT_TAGS": "Etiketler hariç eşleşir",
|
||||||
"TAG_SEPARATOR": "Birden çok virgülle ayrılmış etiket, etiket * veya ** girin",
|
"TAG_SEPARATOR": "Enter multiple comma separated tags, tag*, or **. Optionally include all untagged artifacts when applying the ‘including’ or ‘excluding’ selector by checking the box above.",
|
||||||
"LABELS": "Etiketler",
|
"LABELS": "Etiketler",
|
||||||
"MATCHES_LABELS": "Eşleşen Etiketler",
|
"MATCHES_LABELS": "Eşleşen Etiketler",
|
||||||
"MATCHES_EXCEPT_LABELS": "Etiketler hariç eşleşmeler",
|
"MATCHES_EXCEPT_LABELS": "Etiketler hariç eşleşmeler",
|
||||||
|
@ -250,7 +250,7 @@
|
|||||||
"ROBOT_ACCOUNTS": "机器人账户",
|
"ROBOT_ACCOUNTS": "机器人账户",
|
||||||
"WEBHOOKS": "Webhooks",
|
"WEBHOOKS": "Webhooks",
|
||||||
"IMMUTABLE_TAG": "不可变的Tag",
|
"IMMUTABLE_TAG": "不可变的Tag",
|
||||||
"TAG_STRATEGY": "Tag 策略"
|
"POLICY": "策略"
|
||||||
},
|
},
|
||||||
"PROJECT_CONFIG": {
|
"PROJECT_CONFIG": {
|
||||||
"REGISTRY": "项目仓库",
|
"REGISTRY": "项目仓库",
|
||||||
@ -1246,9 +1246,11 @@
|
|||||||
"IN_REPOSITORIES": "应用到仓库",
|
"IN_REPOSITORIES": "应用到仓库",
|
||||||
"REP_SEPARATOR": "使用逗号分隔repos,repo*和**",
|
"REP_SEPARATOR": "使用逗号分隔repos,repo*和**",
|
||||||
"TAGS": "Tags",
|
"TAGS": "Tags",
|
||||||
|
"INCLUDE_UNTAGGED": " 不含tag 的 artifacts",
|
||||||
|
"UNTAGGED": " 不含tag",
|
||||||
"MATCHES_TAGS": "匹配tags",
|
"MATCHES_TAGS": "匹配tags",
|
||||||
"MATCHES_EXCEPT_TAGS": "排除tags",
|
"MATCHES_EXCEPT_TAGS": "排除tags",
|
||||||
"TAG_SEPARATOR": "使用逗号分割tags,tag*和**",
|
"TAG_SEPARATOR": "输入多个逗号分隔的Tags, Tag*或**。可通过勾选将未加 Tag 的镜像作为此策略的一部分。",
|
||||||
"LABELS": "标签",
|
"LABELS": "标签",
|
||||||
"MATCHES_LABELS": "匹配标签",
|
"MATCHES_LABELS": "匹配标签",
|
||||||
"MATCHES_EXCEPT_LABELS": "排除标签",
|
"MATCHES_EXCEPT_LABELS": "排除标签",
|
||||||
|
Loading…
Reference in New Issue
Block a user