Merge pull request #8936 from bitsf/tag_retention_swagger

add swagger for tag retention
This commit is contained in:
Steven Zou 2019-09-06 19:23:43 +08:00 committed by GitHub
commit 799416d7b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 862 additions and 30 deletions

View File

@ -80,7 +80,7 @@ paths:
type: integer type: integer
format: int32 format: int32
required: false required: false
description: 'The page nubmer, default is 1.' description: 'The page number, default is 1.'
- name: page_size - name: page_size
in: query in: query
type: integer type: integer
@ -280,7 +280,7 @@ paths:
type: integer type: integer
format: int32 format: int32
required: false required: false
description: 'The page nubmer, default is 1.' description: 'The page number, default is 1.'
- name: page_size - name: page_size
in: query in: query
type: integer type: integer
@ -679,7 +679,7 @@ paths:
type: integer type: integer
format: int32 format: int32
required: false required: false
description: 'The page nubmer, default is 1.' description: 'The page number, default is 1.'
- name: page_size - name: page_size
in: query in: query
type: integer type: integer
@ -793,7 +793,7 @@ paths:
type: integer type: integer
format: int32 format: int32
required: false required: false
description: 'The page nubmer, default is 1.' description: 'The page number, default is 1.'
- name: page_size - name: page_size
in: query in: query
type: integer type: integer
@ -1035,7 +1035,7 @@ paths:
type: integer type: integer
format: int32 format: int32
required: false required: false
description: 'The page nubmer, default is 1.' description: 'The page number, default is 1.'
- name: page_size - name: page_size
in: query in: query
type: integer type: integer
@ -1598,7 +1598,7 @@ paths:
type: integer type: integer
format: int32 format: int32
required: false required: false
description: 'The page nubmer, default is 1.' description: 'The page number, default is 1.'
- name: page_size - name: page_size
in: query in: query
type: integer type: integer
@ -3625,7 +3625,7 @@ paths:
type: integer type: integer
format: int32 format: int32
required: false required: false
description: 'The page nubmer, default is 1.' description: 'The page number, default is 1.'
- name: page_size - name: page_size
in: query in: query
type: integer type: integer
@ -3968,6 +3968,277 @@ paths:
description: User have no permission to list webhook jobs of the project. description: User have no permission to list webhook jobs of the project.
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
'/retentions/metadatas':
get:
summary: Get Retention Metadatas
description: Get Retention Metadatas.
tags:
- Products
- Retention
responses:
'200':
description: Get Retention Metadatas successfully.
schema:
type: object
$ref: '#/definitions/RetentionMetadata'
'/retentions':
post:
summary: Create Retention Policy
description: |
Create Retention Policy, you can reference metadatas API for the policy model.
You can check project metadatas to find whether a retention policy is already binded.
This method should only be called when no retention policy binded to project yet.
tags:
- Products
- Retention
parameters:
- name: policy
in: body
description: Create Retention Policy successfully.
required: true
schema:
$ref: '#/definitions/RetentionPolicy'
responses:
'201':
description: Project created successfully.
'400':
description: Illegal format of provided ID value.
'401':
description: User need to log in first.
'403':
description: User have no permission.
'500':
description: Unexpected internal errors.
'/retentions/{id}':
get:
summary: Get Retention Policy
description: Get Retention Policy.
tags:
- Products
- Retention
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Retention ID.
responses:
'200':
description: Get Retention Policy successfully.
schema:
type: object
$ref: '#/definitions/RetentionPolicy'
'401':
description: User need to log in first.
'403':
description: User have no permission.
'500':
description: Unexpected internal errors.
put:
summary: Update Retention Policy
description: |
Update Retention Policy, you can reference metadatas API for the policy model.
You can check project metadatas to find whether a retention policy is already binded.
This method should only be called when retention policy has already binded to project.
tags:
- Products
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Retention ID.
- name: policy
in: body
required: true
schema:
$ref: '#/definitions/RetentionPolicy'
responses:
'200':
description: Update Retention Policy successfully.
'401':
description: User need to log in first.
'403':
description: User have no permission.
'500':
description: Unexpected internal errors.
'/retentions/{id}/executions':
post:
summary: Trigger a Retention job
description: Trigger a Retention job, if dry_run is True, nothing would be deleted actually.
tags:
- Products
- Retention
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Retention ID.
- name: action
in: body
required: true
schema:
type: object
properties:
dry_run:
type: boolean
responses:
'200':
description: Trigger a Retention job successfully.
'401':
description: User need to log in first.
'403':
description: User have no permission.
'500':
description: Unexpected internal errors.
get:
summary: Get a Retention job
description: Get a Retention job, job status may be delayed before job service schedule it up.
tags:
- Products
- Retention
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Retention ID.
responses:
'200':
description: Get a Retention job successfully.
schema:
type: array
items:
type: object
$ref: '#/definitions/RetentionExecution'
'401':
description: User need to log in first.
'403':
description: User have no permission.
'500':
description: Unexpected internal errors.
'/retentions/{id}/executions/{eid}':
patch:
summary: Stop a Retention job
description: Stop a Retention job, only support "stop" action now.
tags:
- Products
- Retention
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Retention ID.
- name: eid
in: path
type: integer
format: int64
required: true
description: Retention execution ID.
- name: action
in: body
description: The action, only support "stop" now.
required: true
schema:
type: object
properties:
action:
type: string
responses:
'200':
description: Stop a Retention job successfully.
'401':
description: User need to log in first.
'403':
description: User have no permission.
'500':
description: Unexpected internal errors.
'/retentions/{id}/executions/{eid}/tasks':
get:
summary: Get Retention job tasks
description: Get Retention job tasks, each repository as a task.
tags:
- Products
- Retention
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Retention ID.
- name: eid
in: path
type: integer
format: int64
required: true
description: Retention execution ID.
responses:
'200':
description: Get Retention job tasks successfully.
schema:
type: array
items:
type: object
$ref: '#/definitions/RetentionExecutionTask'
'401':
description: User need to log in first.
'403':
description: User have no permission.
'500':
description: Unexpected internal errors.
'/retentions/{id}/executions/{eid}/tasks/{tid}':
get:
summary: Get Retention job task log
description: Get Retention job task log, tags ratain or deletion detail will be shown in a table.
tags:
- Products
- Retention
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Retention ID.
- name: eid
in: path
type: integer
format: int64
required: true
description: Retention execution ID.
- name: tid
in: path
type: integer
format: int64
required: true
description: Retention execution ID.
responses:
'200':
description: Get Retention job task log successfully.
schema:
type: string
'401':
description: User need to log in first.
'403':
description: User have no permission.
'500':
description: Unexpected internal errors.
responses: responses:
OK: OK:
description: 'Success' description: 'Success'
@ -4714,7 +4985,7 @@ definitions:
description: The digest of the image. description: The digest of the image.
scan_status: scan_status:
type: string type: string
description: 'The status of the scan job, it can be "pendnig", "running", "finished", "error".' description: 'The status of the scan job, it can be "pending", "running", "finished", "error".'
job_id: job_id:
type: integer type: integer
description: The ID of the job on jobservice to scan the image. description: The ID of the job on jobservice to scan the image.
@ -4904,7 +5175,7 @@ definitions:
properties: properties:
daily_time: daily_time:
type: integer type: integer
description: 'The offest in seconds of UTC 0 o''clock, only valid when the policy type is "daily"' description: 'The offset in seconds of UTC 0 o''clock, only valid when the policy type is "daily"'
description: 'The parameters of the policy, the values are dependant on the type of the policy.' description: 'The parameters of the policy, the values are dependant on the type of the policy.'
ConfigurationsResponse: ConfigurationsResponse:
type: object type: object
@ -5004,7 +5275,7 @@ definitions:
properties: properties:
daily_time: daily_time:
type: integer type: integer
description: 'The offest in seconds of UTC 0 o''clock, only valid when the policy type is "daily"' description: 'The offset in seconds of UTC 0 o''clock, only valid when the policy type is "daily"'
description: 'The parameters of the policy, the values are dependant on the type of the policy.' description: 'The parameters of the policy, the values are dependant on the type of the policy.'
RepositoryDescription: RepositoryDescription:
type: object type: object
@ -5786,3 +6057,195 @@ definitions:
update_time: update_time:
type: string type: string
description: The webhook job update time. description: The webhook job update time.
RetentionMetadata:
type: object
description: the tag retention metadata
properties:
templates:
type: array
description: templates
items:
$ref: '#/definitions/RetentionRuleMetadata'
scope_selectors:
type: array
description: supported scope selectors
items:
$ref: '#/definitions/RetentionSelectorMetadata'
tag_selectors:
type: array
description: supported tag selectors
items:
$ref: '#/definitions/RetentionSelectorMetadata'
RetentionRuleMetadata:
type: object
description: the tag retention rule metadata
properties:
rule_template:
type: string
description: rule id
display_text:
type: string
description: rule display text
action:
type: string
description: rule action
params:
type: array
description: rule params
items:
$ref: '#/definitions/RetentionRuleParamMetadata'
RetentionRuleParamMetadata:
type: object
description: rule param
properties:
type:
type: string
unit:
type: string
required:
type: boolean
RetentionSelectorMetadata:
type: object
description: retention selector
properties:
display_text:
type: string
kind:
type: string
decorations:
type: array
items:
type: string
RetentionPolicy:
type: object
description: retention policy
properties:
id:
type: integer
format: int64
algorithm:
type: string
rules:
type: array
items:
$ref: '#/definitions/RetentionRule'
trigger:
type: object
items:
$ref: '#/definitions/RetentionRuleTrigger'
scope:
type: object
items:
$ref: '#/definitions/RetentionPolicyScope'
RetentionRuleTrigger:
type: object
properties:
kind:
type: string
settings:
type: object
references:
type: object
RetentionPolicyScope:
type: object
properties:
level:
type: string
ref:
type: integer
RetentionRule:
type: object
properties:
id:
type: integer
priority:
type: integer
disabled:
type: boolean
action:
type: string
template:
type: string
params:
type: object
additionalProperties:
type: object
tag_selectors:
type: array
items:
$ref: '#/definitions/RetentionSelector'
scope_selectors:
type: object
additionalProperties:
type: array
items:
$ref: '#/definitions/RetentionSelector'
RetentionSelector:
type: object
properties:
kind:
type: string
decoration:
type: string
pattern:
type: string
RetentionExecution:
type: object
properties:
id:
type: integer
format: int64
policy_id:
type: integer
format: int64
start_time:
type: string
end_time:
type: string
status:
type: string
trigger:
type: string
dry_run:
type: boolean
RetentionExecutionTask:
type: object
properties:
id:
type: integer
format: int64
execution_id:
type: integer
format: int64
repository:
type: string
job_id:
type: string
status:
type: string
status_code:
type: integer
status_revision:
type: integer
format: int64
start_time:
type: string
end_time:
type: string
total:
type: integer
retained:
type: integer

View File

@ -151,6 +151,10 @@ func (r *RetentionAPI) CreateRetention() {
r.SendBadRequestError(err) r.SendBadRequestError(err)
return return
} }
if len(p.Rules) > 15 {
r.SendBadRequestError(errors.New("only 15 rules are allowed at most"))
return
}
if err = r.checkRuleConflict(p); err != nil { if err = r.checkRuleConflict(p); err != nil {
r.SendConflictError(err) r.SendConflictError(err)
return return
@ -176,6 +180,15 @@ func (r *RetentionAPI) CreateRetention() {
r.SendBadRequestError(fmt.Errorf("scope %s is not support", p.Scope.Level)) r.SendBadRequestError(fmt.Errorf("scope %s is not support", p.Scope.Level))
return return
} }
old, err := r.pm.GetMetadataManager().Get(p.Scope.Reference, "retention_id")
if err != nil {
r.SendInternalServerError(err)
return
}
if old != nil && len(old) > 0 {
r.SendBadRequestError(fmt.Errorf("project %v already has retention policy %v", p.Scope.Reference, old["retention_id"]))
return
}
id, err := retentionController.CreateRetention(p) id, err := retentionController.CreateRetention(p)
if err != nil { if err != nil {
r.SendInternalServerError(err) r.SendInternalServerError(err)
@ -202,6 +215,10 @@ func (r *RetentionAPI) UpdateRetention() {
return return
} }
p.ID = id p.ID = id
if len(p.Rules) > 15 {
r.SendBadRequestError(errors.New("only 15 rules are allowed at most"))
return
}
if err = r.checkRuleConflict(p); err != nil { if err = r.checkRuleConflict(p); err != nil {
r.SendConflictError(err) r.SendConflictError(err)
return return
@ -218,14 +235,13 @@ func (r *RetentionAPI) UpdateRetention() {
func (r *RetentionAPI) checkRuleConflict(p *policy.Metadata) error { func (r *RetentionAPI) checkRuleConflict(p *policy.Metadata) error {
temp := make(map[string]int) temp := make(map[string]int)
for n, rule := range p.Rules { for n, rule := range p.Rules {
tid := rule.ID
rule.ID = 0 rule.ID = 0
bs, _ := json.Marshal(rule) bs, _ := json.Marshal(rule)
if old, exists := temp[string(bs)]; exists { if old, exists := temp[string(bs)]; exists {
return fmt.Errorf("rule %d is conflict with rule %d", n, old) return fmt.Errorf("rule %d is conflict with rule %d", n, old)
} }
temp[string(bs)] = n temp[string(bs)] = n
rule.ID = tid rule.ID = n
} }
return nil return nil
} }
@ -276,7 +292,7 @@ func (r *RetentionAPI) OperateRetentionExec() {
return return
} }
a := &struct { a := &struct {
Action string `json:"action" valid:"Required"` Action string `json:"action" valid:"Required;Match(stop)"`
}{} }{}
isValid, err := r.DecodeJSONReqAndValidate(a) isValid, err := r.DecodeJSONReqAndValidate(a)
if !isValid { if !isValid {

View File

@ -1,6 +1,7 @@
package retention package retention
import ( import (
"strings"
"testing" "testing"
"github.com/goharbor/harbor/src/pkg/retention/dep" "github.com/goharbor/harbor/src/pkg/retention/dep"
@ -118,7 +119,8 @@ func (s *ControllerTestSuite) TestPolicy() {
s.Require().Nil(err) s.Require().Nil(err)
p1, err = c.GetRetention(id) p1, err = c.GetRetention(id)
s.Require().Nil(err) s.Require().NotNil(err)
s.Require().True(strings.Contains(err.Error(), "no such Retention policy"))
s.Require().Nil(p1) s.Require().Nil(p1)
} }

View File

@ -111,7 +111,7 @@ func (d *DefaultManager) GetPolicy(id int64) (*policy.Metadata, error) {
p1, err := dao.GetPolicy(id) p1, err := dao.GetPolicy(id)
if err != nil { if err != nil {
if err == orm.ErrNoRows { if err == orm.ErrNoRows {
return nil, nil return nil, fmt.Errorf("no such Retention policy with id %v", id)
} }
return nil, err return nil, err
} }

View File

@ -2,6 +2,7 @@ package retention
import ( import (
"os" "os"
"strings"
"testing" "testing"
"time" "time"
@ -83,7 +84,8 @@ func TestPolicy(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
p1, err = m.GetPolicy(id) p1, err = m.GetPolicy(id)
assert.Nil(t, err) assert.NotNil(t, err)
assert.True(t, strings.Contains(err.Error(), "no such Retention policy"))
assert.Nil(t, p1) assert.Nil(t, p1)
} }

View File

@ -37,7 +37,7 @@ type Execution struct {
StartTime time.Time `json:"start_time"` StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time,omitempty"` EndTime time.Time `json:"end_time,omitempty"`
Status string `json:"status"` Status string `json:"status"`
Trigger string `json:"Trigger"` Trigger string `json:"trigger"`
DryRun bool `json:"dry_run"` DryRun bool `json:"dry_run"`
} }

View File

@ -53,27 +53,24 @@ type Metadata struct {
// Which scope the policy will be applied to // Which scope the policy will be applied to
Scope *Scope `json:"scope" valid:"Required"` Scope *Scope `json:"scope" valid:"Required"`
// The max number of rules in a policy
Capacity int `json:"cap"`
} }
// Valid Valid // Valid Valid
func (m *Metadata) Valid(v *validation.Validation) { func (m *Metadata) Valid(v *validation.Validation) {
if m.Trigger == nil { if m.Trigger == nil {
_ = v.SetError("Trigger", "Trigger is required") _ = v.SetError("Trigger", "Can not be empty")
return return
} }
if m.Scope == nil { if m.Scope == nil {
_ = v.SetError("Scope", "Scope is required") _ = v.SetError("Scope", "Can not be empty")
return return
} }
if m.Trigger != nil && m.Trigger.Kind == TriggerKindSchedule { if m.Trigger != nil && m.Trigger.Kind == TriggerKindSchedule {
if m.Trigger.Settings == nil { if m.Trigger.Settings == nil {
_ = v.SetError("Trigger.Settings", "Trigger.Settings is required") _ = v.SetError("Trigger.Settings", "Can not be empty")
} else { } else {
if _, ok := m.Trigger.Settings[TriggerSettingsCron]; !ok { if _, ok := m.Trigger.Settings[TriggerSettingsCron]; !ok {
_ = v.SetError("Trigger.Settings", "cron in Trigger.Settings is required") _ = v.SetError("Trigger.Settings.cron", "Can not be empty")
} }
} }
} }

View File

@ -37,7 +37,7 @@ type Metadata struct {
Template string `json:"template" valid:"Required"` Template string `json:"template" valid:"Required"`
// The parameters of this rule // The parameters of this rule
Parameters Parameters `json:"params"` Parameters Parameters `json:"params" valid:"Required"`
// Selector attached to the rule for filtering tags // Selector attached to the rule for filtering tags
TagSelectors []*Selector `json:"tag_selectors" valid:"Required"` TagSelectors []*Selector `json:"tag_selectors" valid:"Required"`

View File

@ -116,7 +116,7 @@
<clr-dg-cell class="hand" <clr-dg-cell class="hand"
(click)="openDetail(i,execution.id)">{{execution.dry_run ? 'YES' : 'NO'}}</clr-dg-cell> (click)="openDetail(i,execution.id)">{{execution.dry_run ? 'YES' : 'NO'}}</clr-dg-cell>
<clr-dg-cell class="hand" <clr-dg-cell class="hand"
(click)="openDetail(i,execution.id)">{{execution.Trigger}}</clr-dg-cell> (click)="openDetail(i,execution.id)">{{execution.trigger}}</clr-dg-cell>
<clr-dg-cell class="hand" <clr-dg-cell class="hand"
(click)="openDetail(i,execution.id)">{{execution.start_time|date:'medium'}}</clr-dg-cell> (click)="openDetail(i,execution.id)">{{execution.start_time|date:'medium'}}</clr-dg-cell>
<clr-dg-cell class="hand" (click)="openDetail(i,execution.id)">{{execution.duration}}</clr-dg-cell> <clr-dg-cell class="hand" (click)="openDetail(i,execution.id)">{{execution.duration}}</clr-dg-cell>

View File

@ -166,6 +166,13 @@ export class TagRetentionComponent implements OnInit {
if (this.retentionId) { if (this.retentionId) {
this.tagRetentionService.getRetention(this.retentionId).subscribe( this.tagRetentionService.getRetention(this.retentionId).subscribe(
response => { response => {
if (response && response.rules && response.rules.length > 0) {
response.rules.forEach(item => {
if (!item.params) {
item.params = {};
}
});
}
this.retention = response; this.retention = response;
this.loadingRule = false; this.loadingRule = false;
}, error => { }, error => {

View File

@ -39,7 +39,7 @@ def _random_name(prefix):
def _get_id_from_header(header): def _get_id_from_header(header):
try: try:
location = header["Location"] location = header["Location"]
return location.split("/")[-1] return int(location.split("/")[-1])
except Exception: except Exception:
return None return None

View File

@ -12,6 +12,7 @@ except ImportError:
class DockerAPI(object): class DockerAPI(object):
def __init__(self): def __init__(self):
self.DCLIENT = docker.APIClient(base_url='unix://var/run/docker.sock',version='auto',timeout=10) self.DCLIENT = docker.APIClient(base_url='unix://var/run/docker.sock',version='auto',timeout=10)
self.DCLIENT2 = docker.from_env()
def docker_login(self, registry, username, password, expected_error_message = None): def docker_login(self, registry, username, password, expected_error_message = None):
if expected_error_message is "": if expected_error_message is "":
@ -84,4 +85,44 @@ class DockerAPI(object):
raise Exception(r" Failed to catch error [{}] when push image {}".format (expected_error_message, harbor_registry)) raise Exception(r" Failed to catch error [{}] when push image {}".format (expected_error_message, harbor_registry))
else: else:
if str(ret).lower().find("errorDetail".lower()) >= 0: if str(ret).lower().find("errorDetail".lower()) >= 0:
raise Exception(r" It's was not suppose to catch error when push image {}, return message is [{}]".format (harbor_registry, ret)) raise Exception(r" It's was not suppose to catch error when push image {}, return message is [{}]".format (harbor_registry, ret))
def docker_image_build(self, harbor_registry, tags=None, size=1, expected_error_message = None):
caught_err = False
ret = ""
try:
baseimage='busybox:latest'
if not self.DCLIENT.images(name=baseimage):
self.DCLIENT.pull(baseimage)
c=self.DCLIENT.create_container(image='busybox:latest',command='dd if=/dev/urandom of=test bs=1M count=%d' % size )
self.DCLIENT.start(c)
self.DCLIENT.wait(c)
if not tags:
tags=['latest']
firstrepo="%s:%s" % (harbor_registry, tags[0])
#self.DCLIENT.commit(c, firstrepo)
self.DCLIENT2.containers.get(c).commit(harbor_registry, tags[0])
for tag in tags[1:]:
repo="%s:%s" % (harbor_registry, tag)
self.DCLIENT.tag(firstrepo, repo)
for tag in tags:
repo="%s:%s" % (harbor_registry, tag)
self.DCLIENT.push(repo)
print("build image %s with size %d" % (repo, size))
self.DCLIENT.remove_image(repo)
self.DCLIENT.remove_container(c)
except Exception, err:
caught_err = True
if expected_error_message is not None:
print "docker image build error:", str(err)
if str(err).lower().find(expected_error_message.lower()) < 0:
raise Exception(r"Push image: Return message {} is not as expected {}".format(str(err), expected_error_message))
else:
raise Exception(r" Docker build image {} failed, error is [{}]".format (harbor_registry, err.message))
if caught_err == False:
if expected_error_message is not None:
if str(ret).lower().find(expected_error_message.lower()) < 0:
raise Exception(r" Failed to catch error [{}] when push image {}".format (expected_error_message, harbor_registry))
else:
if str(ret).lower().find("errorDetail".lower()) >= 0:
raise Exception(r" It's was not suppose to catch error when push image {}, return message is [{}]".format (harbor_registry, ret))

View File

@ -30,6 +30,14 @@ def push_image_to_project(project_name, registry, username, password, image, tag
return r'{}/{}'.format(project_name, image), new_tag return r'{}/{}'.format(project_name, image), new_tag
def push_special_image_to_project(project_name, registry, username, password, image, tags=None, size=1, expected_login_error_message=None, expected_error_message = None):
_docker_api = DockerAPI()
_docker_api.docker_login(registry, username, password, expected_error_message = expected_login_error_message)
time.sleep(2)
if expected_login_error_message != None:
return
_docker_api.docker_image_build(r'{}/{}/{}'.format(registry, project_name, image), tags = tags, size=size, expected_error_message=expected_error_message)
def is_repo_exist_in_project(repositories, repo_name): def is_repo_exist_in_project(repositories, repo_name):
result = False result = False
for reop in repositories: for reop in repositories:

View File

@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
import base
import swagger_client
class Retention(base.Base):
def get_metadatas(self, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
metadatas, status_code, _ = client.retentions_metadatas_get_with_http_info()
base._assert_status_code(expect_status_code, status_code)
return metadatas
def create_retention_policy(self, project_id, selector_repository="**", selector_tag="**", expect_status_code = 201, **kwargs):
policy=swagger_client.RetentionPolicy(
algorithm='or',
rules=[
swagger_client.RetentionRule(
disabled=False,
action="retain",
template="always",
params= {
},
scope_selectors={
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": selector_repository
}
]
},
tag_selectors=[
{
"kind": "doublestar",
"decoration": "matches",
"pattern": selector_tag
}
]
)
],
trigger= {
"kind": "Schedule",
"settings": {
"cron": ""
},
"references": {
}
},
scope= {
"level": "project",
"ref": project_id
},
)
client = self._get_client(**kwargs)
_, status_code, header = client.retentions_post_with_http_info(policy)
base._assert_status_code(expect_status_code, status_code)
return base._get_id_from_header(header)
def get_retention_policy(self, retention_id, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
policy, status_code, _ = client.retentions_id_get_with_http_info(retention_id)
base._assert_status_code(expect_status_code, status_code)
return policy
def update_retention_policy(self, retention_id, selector_repository="**", selector_tag="**", expect_status_code = 200, **kwargs):
policy=swagger_client.RetentionPolicy(
id=retention_id,
algorithm='or',
rules=[
swagger_client.RetentionRule(
disabled=False,
action="retain",
template="always",
params= {
},
scope_selectors={
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": selector_repository
}
]
},
tag_selectors=[
{
"kind": "doublestar",
"decoration": "matches",
"pattern": selector_tag
}
]
)
],
trigger= {
"kind": "Schedule",
"settings": {
"cron": ""
},
"references": {
}
},
scope= {
"level": "project",
"ref": project_id
},
)
client = self._get_client(**kwargs)
_, status_code, _ = client.retentions_id_put_with_http_info(retention_id, policy)
base._assert_status_code(expect_status_code, status_code)
def update_retention_add_rule(self, retention_id, selector_repository="**", selector_tag="**", expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
policy, status_code, _ = client.retentions_id_get_with_http_info(retention_id)
base._assert_status_code(200, status_code)
policy.rules.append(swagger_client.RetentionRule(
disabled=False,
action="retain",
template="always",
params= {
},
scope_selectors={
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": selector_repository
}
]
},
tag_selectors=[
{
"kind": "doublestar",
"decoration": "matches",
"pattern": selector_tag
}
]
))
_, status_code, _ = client.retentions_id_put_with_http_info(retention_id, policy)
base._assert_status_code(expect_status_code, status_code)
def trigger_retention_policy(self, retention_id, dry_run=False, expect_status_code = 201, **kwargs):
client = self._get_client(**kwargs)
_, status_code, _ = client.retentions_id_executions_post_with_http_info(retention_id, {"dry_run":dry_run})
base._assert_status_code(expect_status_code, status_code)
def stop_retention_execution(self, retention_id, exec_id, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
r, status_code, _ = client.retentions_id_executions_eid_patch(retention_id, exec_id, {"action":"stop"})
base._assert_status_code(expect_status_code, status_code)
return r
def get_retention_executions(self, retention_id, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
r, status_code, _ = client.retentions_id_executions_get_with_http_info(retention_id)
base._assert_status_code(expect_status_code, status_code)
return r
def get_retention_exec_tasks(self, retention_id, exec_id, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
r, status_code, _ = client.retentions_id_executions_eid_tasks_get_with_http_info(retention_id, exec_id)
base._assert_status_code(expect_status_code, status_code)
return r
def get_retention_exec_task_log(self, retention_id, exec_id, task_id, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
r, status_code, _ = client.retentions_id_executions_eid_tasks_tid_get_with_http_info(retention_id, exec_id, task_id)
base._assert_status_code(expect_status_code, status_code)
return r
def get_retention_metadatas(self, expect_status_code = 200, **kwargs):
client = self._get_client(**kwargs)
r, status_code, _ = client.retentions_metadatas_get_with_http_info()
base._assert_status_code(expect_status_code, status_code)
return r

View File

@ -0,0 +1,110 @@
from __future__ import absolute_import
import unittest
import time
from testutils import ADMIN_CLIENT
from testutils import TEARDOWN
from testutils import harbor_server
from library.repository import push_special_image_to_project
from library.retention import Retention
from library.project import Project
from library.repository import Repository
from library.user import User
from library.system import System
class TestProjects(unittest.TestCase):
"""
Test case:
Tag Retention
Setup:
Create Project test-retention
Push image test1:1.0, test1:2.0, test1:3.0,latest, test2:1.0, test2:latest, test3:1.0, test4:1.0
Test Steps:
1. Create Retention Policy
2. Add rule "For the repositories matching **, retain always with tags matching latest*"
3. Add rule "For the repositories matching test3*, retain always with tags matching **"
4. Dry run, check execution and tasks
5. Real run, check images retained
Tear Down:
1. Delete project test-retention
"""
@classmethod
def setUpClass(self):
self.user = User()
self.system = System()
self.repo= Repository()
self.project = Project()
self.retention=Retention()
def testTagRetention(self):
user_ra_password = "Aa123456"
user_ra_id, user_ra_name = self.user.create_user(user_password=user_ra_password, **ADMIN_CLIENT)
print("Created user: %s, id: %s" % (user_ra_name, user_ra_id))
TestProjects.USER_RA_CLIENT = dict(endpoint=ADMIN_CLIENT["endpoint"],
username=user_ra_name,
password=user_ra_password)
TestProjects.user_ra_id = int(user_ra_id)
TestProjects.project_src_repo_id, project_src_repo_name = self.project.create_project(metadata = {"public": "false"}, **TestProjects.USER_RA_CLIENT)
# Push image test1:1.0, test1:2.0, test1:3.0,latest, test2:1.0, test2:latest, test3:1.0
push_special_image_to_project(project_src_repo_name, harbor_server, user_ra_name, user_ra_password, "test1", ['1.0'])
push_special_image_to_project(project_src_repo_name, harbor_server, user_ra_name, user_ra_password, "test1", ['2.0'])
push_special_image_to_project(project_src_repo_name, harbor_server, user_ra_name, user_ra_password, "test1", ['3.0','latest'])
push_special_image_to_project(project_src_repo_name, harbor_server, user_ra_name, user_ra_password, "test2", ['1.0'])
push_special_image_to_project(project_src_repo_name, harbor_server, user_ra_name, user_ra_password, "test2", ['latest'])
push_special_image_to_project(project_src_repo_name, harbor_server, user_ra_name, user_ra_password, "test3", ['1.0'])
push_special_image_to_project(project_src_repo_name, harbor_server, user_ra_name, user_ra_password, "test4", ['1.0'])
resp=self.repo.get_repository(TestProjects.project_src_repo_id, **TestProjects.USER_RA_CLIENT)
self.assertEqual(len(resp), 4)
# Create Retention Policy
retention_id = self.retention.create_retention_policy(TestProjects.project_src_repo_id, selector_repository="**", selector_tag="latest*", expect_status_code = 201, **TestProjects.USER_RA_CLIENT)
# Add rule
self.retention.update_retention_add_rule(retention_id,selector_repository="test3*", selector_tag="**", expect_status_code = 200, **TestProjects.USER_RA_CLIENT)
# Dry run
self.retention.trigger_retention_policy(retention_id, dry_run=True, **TestProjects.USER_RA_CLIENT)
time.sleep(2)
resp=self.retention.get_retention_executions(retention_id, **TestProjects.USER_RA_CLIENT)
self.assertTrue(len(resp)>0)
execution=resp[0]
resp=self.retention.get_retention_exec_tasks(retention_id,execution.id, **TestProjects.USER_RA_CLIENT)
self.assertEqual(len(resp), 4)
resp=self.retention.get_retention_exec_task_log(retention_id,execution.id,resp[0].id, **TestProjects.USER_RA_CLIENT)
print(resp)
# Real run
self.retention.trigger_retention_policy(retention_id, dry_run=False, **TestProjects.USER_RA_CLIENT)
time.sleep(10)
resp=self.retention.get_retention_executions(retention_id, **TestProjects.USER_RA_CLIENT)
self.assertTrue(len(resp)>1)
execution=resp[0]
resp=self.retention.get_retention_exec_tasks(retention_id,execution.id, **TestProjects.USER_RA_CLIENT)
self.assertEqual(len(resp), 4)
resp=self.retention.get_retention_exec_task_log(retention_id,execution.id,resp[0].id, **TestProjects.USER_RA_CLIENT)
print(resp)
resp=self.repo.get_repository(TestProjects.project_src_repo_id, **TestProjects.USER_RA_CLIENT)
self.assertEqual(len(resp), 3)
@classmethod
def tearDownClass(self):
print "Case completed"
@unittest.skipIf(TEARDOWN == False, "Test data won't be erased.")
def test_ClearData(self):
resp=self.repo.get_repository(TestProjects.project_src_repo_id, **TestProjects.USER_RA_CLIENT)
for repo in resp:
self.repo.delete_repoitory(repo.name, **TestProjects.USER_RA_CLIENT)
self.project.delete_project(TestProjects.project_src_repo_id, **TestProjects.USER_RA_CLIENT)
self.user.delete_user(TestProjects.user_ra_id, **ADMIN_CLIENT)
if __name__ == '__main__':
unittest.main()

View File

@ -2,7 +2,7 @@ import time
import os import os
import sys import sys
sys.path.append(os.environ["SWAGGER_CLIENT_PATH"]) sys.path.insert(0, os.environ["SWAGGER_CLIENT_PATH"])
from swagger_client.rest import ApiException from swagger_client.rest import ApiException
import swagger_client.models import swagger_client.models
from pprint import pprint from pprint import pprint
@ -12,7 +12,7 @@ admin_pwd = "Harbor12345"
harbor_server = os.environ["HARBOR_HOST"] harbor_server = os.environ["HARBOR_HOST"]
#CLIENT=dict(endpoint="https://"+harbor_server+"/api") #CLIENT=dict(endpoint="https://"+harbor_server+"/api")
ADMIN_CLIENT=dict(endpoint = "https://"+harbor_server+"/api", username = admin_user, password = admin_pwd) ADMIN_CLIENT=dict(endpoint = os.environ.get("HARBOR_HOST_SCHEMA", "https")+ "://"+harbor_server+"/api", username = admin_user, password = admin_pwd)
USER_ROLE=dict(admin=0,normal=1) USER_ROLE=dict(admin=0,normal=1)
TEARDOWN = True TEARDOWN = True

View File

@ -53,3 +53,6 @@ Test Case - System Level CVE Whitelist
Harbor API Test ./tests/apitests/python/test_sys_cve_whitelists.py Harbor API Test ./tests/apitests/python/test_sys_cve_whitelists.py
Test Case - Project Level CVE Whitelist Test Case - Project Level CVE Whitelist
Harbor API Test ./tests/apitests/python/test_project_level_cve_whitelist.py Harbor API Test ./tests/apitests/python/test_project_level_cve_whitelist.py
Test Case - Tag Retention
Harbor API Test ./tests/apitests/python/test_retention.py