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
|
||||
type Factory func(decoration string, pattern string) Selector
|
||||
type Factory func(decoration string, pattern string, extras string) Selector
|
||||
|
@ -15,6 +15,7 @@
|
||||
package doublestar
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/bmatcuk/doublestar"
|
||||
iselector "github.com/goharbor/harbor/src/internal/selector"
|
||||
)
|
||||
@ -43,6 +44,8 @@ type selector struct {
|
||||
decoration string
|
||||
// The pattern expression
|
||||
pattern string
|
||||
// whether match untagged
|
||||
untagged bool
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for _, t := range artifact.Tags {
|
||||
matched, err := match(s.pattern, t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if matched {
|
||||
return true, nil
|
||||
if len(artifact.Tags) > 0 {
|
||||
for _, t := range artifact.Tags {
|
||||
matched, err := match(s.pattern, t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
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) {
|
||||
for _, t := range artifact.Tags {
|
||||
matched, err := match(s.pattern, t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !matched {
|
||||
return true, nil
|
||||
if len(artifact.Tags) > 0 {
|
||||
for _, t := range artifact.Tags {
|
||||
matched, err := match(s.pattern, t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !matched {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return false, nil
|
||||
return !s.untagged, nil
|
||||
}
|
||||
|
||||
// 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{
|
||||
decoration: decoration,
|
||||
pattern: pattern,
|
||||
untagged: untagged,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,16 @@ func (suite *RegExpSelectorTestSuite) SetupSuite() {
|
||||
CreationTime: time.Now().Unix() - 7200,
|
||||
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 {
|
||||
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
|
||||
@ -107,15 +126,23 @@ func (suite *RegExpSelectorTestSuite) TestTagExcludes() {
|
||||
tagExcludes := &selector{
|
||||
decoration: Excludes,
|
||||
pattern: "{latest,4.*}",
|
||||
untagged: true,
|
||||
}
|
||||
|
||||
selected, err := tagExcludes.Select(suite.artifacts)
|
||||
require.NoError(suite.T(), err)
|
||||
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{
|
||||
decoration: Excludes,
|
||||
pattern: "4.*",
|
||||
untagged: true,
|
||||
}
|
||||
|
||||
selected, err = tagExcludes2.Select(suite.artifacts)
|
||||
@ -162,7 +189,7 @@ func (suite *RegExpSelectorTestSuite) TestRepoExcludes() {
|
||||
|
||||
selected, err := repoExcludes.Select(suite.artifacts)
|
||||
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 {
|
||||
return expect([]string{"harbor:latest"}, selected)
|
||||
})
|
||||
@ -174,7 +201,7 @@ func (suite *RegExpSelectorTestSuite) TestRepoExcludes() {
|
||||
|
||||
selected, err = repoExcludes2.Select(suite.artifacts)
|
||||
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 {
|
||||
return expect([]string{"redis:4.0", "redis:4.1"}, selected)
|
||||
})
|
||||
@ -189,7 +216,7 @@ func (suite *RegExpSelectorTestSuite) TestNSMatches() {
|
||||
|
||||
selected, err := repoMatches.Select(suite.artifacts)
|
||||
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 {
|
||||
return expect([]string{"harbor:latest"}, selected)
|
||||
})
|
||||
@ -228,7 +255,7 @@ func (suite *RegExpSelectorTestSuite) TestNSExcludes() {
|
||||
|
||||
selected, err = repoExcludes2.Select(suite.artifacts)
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
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 {
|
||||
if dec == decoration {
|
||||
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
|
||||
func New(decoration string, pattern string) iselector.Selector {
|
||||
func New(decoration string, pattern string, extras string) iselector.Selector {
|
||||
labels := make([]string, 0)
|
||||
if len(pattern) > 0 {
|
||||
labels = append(labels, strings.Split(pattern, ",")...)
|
||||
|
@ -33,7 +33,7 @@ func (rm *Matcher) Match(pid int64, c iselector.Candidate) (bool, error) {
|
||||
}
|
||||
repositorySelector := repositorySelectors[0]
|
||||
selector, err := index.Get(repositorySelector.Kind, repositorySelector.Decoration,
|
||||
repositorySelector.Pattern)
|
||||
repositorySelector.Pattern, "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -53,7 +53,7 @@ func (rm *Matcher) Match(pid int64, c iselector.Candidate) (bool, error) {
|
||||
}
|
||||
tagSelector := r.TagSelectors[0]
|
||||
selector, err = index.Get(tagSelector.Kind, tagSelector.Decoration,
|
||||
tagSelector.Pattern)
|
||||
tagSelector.Pattern, "")
|
||||
if err != nil {
|
||||
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
|
||||
for _, projectSelector := range rule.ScopeSelectors["project"] {
|
||||
selector, err := index.Get(projectSelector.Kind, projectSelector.Decoration,
|
||||
projectSelector.Pattern)
|
||||
projectSelector.Pattern, "")
|
||||
if err != nil {
|
||||
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
|
||||
for _, repositorySelector := range rule.ScopeSelectors["repository"] {
|
||||
selector, err := index.Get(repositorySelector.Kind, repositorySelector.Decoration,
|
||||
repositorySelector.Pattern)
|
||||
repositorySelector.Pattern, repositorySelector.Extras)
|
||||
if err != nil {
|
||||
return 0, launcherError(err)
|
||||
}
|
||||
|
@ -93,8 +93,8 @@ func (suite *ProcessorTestSuite) TestProcess() {
|
||||
params = append(params, &alg.Parameter{
|
||||
Evaluator: lastx.New(lastxParams),
|
||||
Selectors: []selector.Selector{
|
||||
doublestar.New(doublestar.Matches, "*dev*"),
|
||||
label.New(label.With, "L1,L2"),
|
||||
doublestar.New(doublestar.Matches, "*dev*", ""),
|
||||
label.New(label.With, "L1,L2", ""),
|
||||
},
|
||||
Performer: perf,
|
||||
})
|
||||
@ -104,7 +104,7 @@ func (suite *ProcessorTestSuite) TestProcess() {
|
||||
params = append(params, &alg.Parameter{
|
||||
Evaluator: latestps.New(latestKParams),
|
||||
Selectors: []selector.Selector{
|
||||
label.New(label.With, "L3"),
|
||||
label.New(label.With, "L3", ""),
|
||||
},
|
||||
Performer: perf,
|
||||
})
|
||||
@ -134,8 +134,8 @@ func (suite *ProcessorTestSuite) TestProcess2() {
|
||||
params = append(params, &alg.Parameter{
|
||||
Evaluator: always.New(alwaysParams),
|
||||
Selectors: []selector.Selector{
|
||||
doublestar.New(doublestar.Matches, "latest"),
|
||||
label.New(label.With, ""),
|
||||
doublestar.New(doublestar.Matches, "latest", ""),
|
||||
label.New(label.With, "", ""),
|
||||
},
|
||||
Performer: perf,
|
||||
})
|
||||
|
@ -78,7 +78,7 @@ func (bb *basicBuilder) Build(policy *lwp.Metadata, isDryRun bool) (alg.Processo
|
||||
|
||||
sl := make([]selector.Selector, 0)
|
||||
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 {
|
||||
return nil, errors.Wrap(err, "get selector by metadata")
|
||||
}
|
||||
|
@ -75,6 +75,9 @@ type Selector struct {
|
||||
|
||||
// Param for the selector
|
||||
Pattern string `json:"pattern" valid:"Required"`
|
||||
|
||||
// Extras for other settings
|
||||
Extras string `json:"extras"`
|
||||
}
|
||||
|
||||
// Parameters of rule, indexed by the key
|
||||
|
@ -87,7 +87,7 @@ export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
{
|
||||
linkName: "tag-strategy",
|
||||
tabLinkInOverflow: false,
|
||||
showTabName: "PROJECT_DETAIL.TAG_STRATEGY",
|
||||
showTabName: "PROJECT_DETAIL.POLICY",
|
||||
permissions: () => this.hasTagRetentionPermission
|
||||
},
|
||||
{
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div class="clr-col-4">
|
||||
<span>{{'TAG_RETENTION.IN_REPOSITORIES' | translate}}</span>
|
||||
</div>
|
||||
<div class="clr-col-3">
|
||||
<div class="clr-col-2">
|
||||
<div class="clr-select-wrapper w-100">
|
||||
<select [(ngModel)]="repoSelect" class="clr-select w-100">
|
||||
<option *ngFor="let d of metadata?.scope_selectors[0]?.decorations"
|
||||
@ -18,7 +18,7 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-col-5">
|
||||
<div class="clr-col-6">
|
||||
<div class="w-100">
|
||||
<input id="repos" required [(ngModel)]="repositories" class="clr-input w-100">
|
||||
</div>
|
||||
@ -57,12 +57,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="height-72">
|
||||
<div class="height-85">
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-4">
|
||||
<label>{{'TAG_RETENTION.TAGS' | translate}}</label>
|
||||
</div>
|
||||
<div class="clr-col-3">
|
||||
<div class="clr-col-2">
|
||||
<div class="clr-select-wrapper w-100">
|
||||
<select [(ngModel)]="tagsSelect" class="clr-select w-100">
|
||||
<option *ngFor="let d of metadata?.tag_selectors[0]?.decorations"
|
||||
@ -70,16 +70,22 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-col-5">
|
||||
<div class="clr-col-3">
|
||||
<div class="w-100">
|
||||
<input id="tags" required [(ngModel)]="tagsInput" class="clr-input w-100">
|
||||
</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 class="clr-row">
|
||||
<div class="clr-col-4"></div>
|
||||
<div class="clr-col-8">
|
||||
<span>{{'TAG_RETENTION.TAG_SEPARATOR' | translate}}</span>
|
||||
<span class="tootip">{{'TAG_RETENTION.TAG_SEPARATOR' | translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,7 +11,25 @@
|
||||
.height-72 {
|
||||
height: 72px;
|
||||
}
|
||||
.height-85 {
|
||||
height: 85px;
|
||||
}
|
||||
|
||||
.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() {
|
||||
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() {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -96,6 +96,7 @@ export class Rule extends BaseRule {
|
||||
constructor() {
|
||||
super();
|
||||
this.params = {};
|
||||
this.tag_selectors[0].extras = JSON.stringify({untagged: true});
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,6 +104,7 @@ export class Selector {
|
||||
kind: string;
|
||||
decoration: string;
|
||||
pattern: string;
|
||||
extras?: string;
|
||||
}
|
||||
|
||||
export class Param {
|
||||
|
@ -44,6 +44,9 @@
|
||||
<span>{{'TAG_RETENTION.LOWER_TAGS' | translate}}</span>
|
||||
<span>{{getI18nKey(rule?.tag_selectors[0]?.decoration)|translate}}</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">
|
||||
<span class="color-97">{{'TAG_RETENTION.AND' | translate}}</span>
|
||||
<span>{{'TAG_RETENTION.LOWER_LABELS' | translate}}</span>
|
||||
|
@ -37,6 +37,10 @@ const SCHEDULE_TYPE = {
|
||||
HOURLY: "Hourly",
|
||||
CUSTOM: "Custom"
|
||||
};
|
||||
const DECORATION = {
|
||||
MATCHES: "matches",
|
||||
EXCLUDES: "excludes",
|
||||
};
|
||||
const RUNNING: string = "Running";
|
||||
const PENDING: string = "pending";
|
||||
const TIMEOUT: number = 5000;
|
||||
@ -176,6 +180,8 @@ export class TagRetentionComponent implements OnInit {
|
||||
if (!item.params) {
|
||||
item.params = {};
|
||||
}
|
||||
this.setRuleUntagged(item);
|
||||
|
||||
});
|
||||
}
|
||||
this.retention = response;
|
||||
@ -225,7 +231,28 @@ export class TagRetentionComponent implements OnInit {
|
||||
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() {
|
||||
this.addRuleComponent.open();
|
||||
this.addRuleComponent.isAdd = true;
|
||||
@ -450,4 +477,11 @@ export class TagRetentionComponent implements OnInit {
|
||||
|
||||
this.refreshList();
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param extras Json string
|
||||
*/
|
||||
showUntagged(extras) {
|
||||
return JSON.parse(extras).untagged;
|
||||
}
|
||||
}
|
||||
|
@ -251,7 +251,7 @@
|
||||
"ROBOT_ACCOUNTS": "Robot Accounts",
|
||||
"WEBHOOKS": "Webhooks",
|
||||
"IMMUTABLE_TAG": "Tag Immutability",
|
||||
"TAG_STRATEGY": "Tag Strategy"
|
||||
"POLICY": "Policy"
|
||||
},
|
||||
"PROJECT_CONFIG": {
|
||||
"REGISTRY": "Project registry",
|
||||
@ -1250,9 +1250,11 @@
|
||||
"IN_REPOSITORIES": "For the repositories",
|
||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||
"TAGS": "Tags",
|
||||
"UNTAGGED": " untagged",
|
||||
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||
"MATCHES_TAGS": "Matches 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",
|
||||
"MATCHES_LABELS": "Matches Labels",
|
||||
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
||||
@ -1309,7 +1311,7 @@
|
||||
"IN_REPOSITORIES": "For the repositories",
|
||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||
"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",
|
||||
"EXC": " excluding ",
|
||||
"MAT": " matching ",
|
||||
|
@ -252,7 +252,7 @@
|
||||
"ROBOT_ACCOUNTS": "Robot Accounts",
|
||||
"WEBHOOKS": "Webhooks",
|
||||
"IMMUTABLE_TAG": "Tag Immutability",
|
||||
"TAG_STRATEGY": "Tag Strategy"
|
||||
"POLICY": "Policy"
|
||||
},
|
||||
"PROJECT_CONFIG": {
|
||||
"REGISTRY": "Registro de proyectos",
|
||||
@ -1247,9 +1247,11 @@
|
||||
"IN_REPOSITORIES": "For the repositories",
|
||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||
"TAGS": "Tags",
|
||||
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||
"UNTAGGED": " untagged",
|
||||
"MATCHES_TAGS": "Matches 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",
|
||||
"MATCHES_LABELS": "Matches Labels",
|
||||
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
||||
|
@ -245,7 +245,7 @@
|
||||
"ROBOT_ACCOUNTS": "Robot Accounts",
|
||||
"WEBHOOKS": "Webhooks",
|
||||
"IMMUTABLE_TAG": "Tag Immutability",
|
||||
"TAG_STRATEGY": "Tag Strategy"
|
||||
"POLICY": "Policy"
|
||||
},
|
||||
"PROJECT_CONFIG": {
|
||||
"REGISTRY": "Dépôt du Projet",
|
||||
@ -1217,9 +1217,11 @@
|
||||
"IN_REPOSITORIES": "For the repositories",
|
||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||
"TAGS": "Tags",
|
||||
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||
"UNTAGGED": " untagged",
|
||||
"MATCHES_TAGS": "Matches 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",
|
||||
"MATCHES_LABELS": "Matches Labels",
|
||||
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
||||
|
@ -249,7 +249,7 @@
|
||||
"ROBOT_ACCOUNTS": "Robot Accounts",
|
||||
"WEBHOOKS": "Webhooks",
|
||||
"IMMUTABLE_TAG": "Tag Immutability",
|
||||
"TAG_STRATEGY": "Tag Strategy"
|
||||
"POLICY": "Policy"
|
||||
},
|
||||
"PROJECT_CONFIG": {
|
||||
"REGISTRY": "Registro do Projeto",
|
||||
@ -1245,9 +1245,11 @@
|
||||
"IN_REPOSITORIES": "For the repositories",
|
||||
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
|
||||
"TAGS": "Tags",
|
||||
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||
"UNTAGGED": " untagged",
|
||||
"MATCHES_TAGS": "Matches 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",
|
||||
"MATCHES_LABELS": "Matches Labels",
|
||||
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
|
||||
|
@ -251,7 +251,7 @@
|
||||
"ROBOT_ACCOUNTS": "Robot Hesapları",
|
||||
"WEBHOOKS": "Ağ Kancaları",
|
||||
"IMMUTABLE_TAG": "Tag Immutability",
|
||||
"TAG_STRATEGY": "Tag Strategy"
|
||||
"POLICY": "Policy"
|
||||
},
|
||||
"PROJECT_CONFIG": {
|
||||
"REGISTRY": "Proje kaydı",
|
||||
@ -1249,9 +1249,11 @@
|
||||
"IN_REPOSITORIES": "Depolar için",
|
||||
"REP_SEPARATOR": "Birden çok virgülle ayrılmış depolar, depo* veya ** girin",
|
||||
"TAGS": "Etiketler",
|
||||
"INCLUDE_UNTAGGED": " untagged artifacts",
|
||||
"UNTAGGED": " untagged",
|
||||
"MATCHES_TAGS": "Etiketleri 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",
|
||||
"MATCHES_LABELS": "Eşleşen Etiketler",
|
||||
"MATCHES_EXCEPT_LABELS": "Etiketler hariç eşleşmeler",
|
||||
|
@ -250,7 +250,7 @@
|
||||
"ROBOT_ACCOUNTS": "机器人账户",
|
||||
"WEBHOOKS": "Webhooks",
|
||||
"IMMUTABLE_TAG": "不可变的Tag",
|
||||
"TAG_STRATEGY": "Tag 策略"
|
||||
"POLICY": "策略"
|
||||
},
|
||||
"PROJECT_CONFIG": {
|
||||
"REGISTRY": "项目仓库",
|
||||
@ -1246,9 +1246,11 @@
|
||||
"IN_REPOSITORIES": "应用到仓库",
|
||||
"REP_SEPARATOR": "使用逗号分隔repos,repo*和**",
|
||||
"TAGS": "Tags",
|
||||
"INCLUDE_UNTAGGED": " 不含tag 的 artifacts",
|
||||
"UNTAGGED": " 不含tag",
|
||||
"MATCHES_TAGS": "匹配tags",
|
||||
"MATCHES_EXCEPT_TAGS": "排除tags",
|
||||
"TAG_SEPARATOR": "使用逗号分割tags,tag*和**",
|
||||
"TAG_SEPARATOR": "输入多个逗号分隔的Tags, Tag*或**。可通过勾选将未加 Tag 的镜像作为此策略的一部分。",
|
||||
"LABELS": "标签",
|
||||
"MATCHES_LABELS": "匹配标签",
|
||||
"MATCHES_EXCEPT_LABELS": "排除标签",
|
||||
|
Loading…
Reference in New Issue
Block a user