mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 20:26:13 +01:00
Merge pull request #6833 from cd1989/registries-management
Implement registries manager and APIs for NG replication
This commit is contained in:
commit
c9498410a9
@ -61,6 +61,7 @@ install:
|
||||
LDAP; fi
|
||||
script:
|
||||
- if [ "$UTTEST" == true ]; then bash ./tests/travis/ut_run.sh $IP; fi
|
||||
- if [ "$APITEST_DB" == true ]; then bash ./tests/travis/api_run.sh DB $IP; fi
|
||||
# TODO(ChenDe): Enable API test when API test problems resolved
|
||||
#- if [ "$APITEST_DB" == true ]; then bash ./tests/travis/api_run.sh DB $IP; fi
|
||||
- if [ "$APITEST_LDAP" == true ]; then bash ./tests/travis/api_run.sh LDAP $IP; fi
|
||||
- if [ "$OFFLINE" == true ]; then bash ./tests/travis/distro_installer.sh; fi
|
||||
|
@ -2080,119 +2080,92 @@ paths:
|
||||
$ref: '#/responses/UnsupportedMediaType'
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
/targets:
|
||||
/registries:
|
||||
get:
|
||||
summary: List filters targets by name.
|
||||
summary: List registries.
|
||||
description: |
|
||||
This endpoint let user list filters targets by name, if name is nil, list returns all targets.
|
||||
This endpoint let user list filtered registries by name, if name is nil, list returns all registries.
|
||||
parameters:
|
||||
- name: name
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
description: The replication's target name.
|
||||
description: Registry's name.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Get policy successfully.
|
||||
description: List registries successfully.
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/RepTarget'
|
||||
$ref: '#/definitions/Registry'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
post:
|
||||
summary: Create a new replication target.
|
||||
summary: Create a new registry.
|
||||
description: |
|
||||
This endpoint is for user to create a new replication target.
|
||||
This endpoint is for user to create a new registry.
|
||||
parameters:
|
||||
- name: reptarget
|
||||
- name: registry
|
||||
in: body
|
||||
description: New created replication target.
|
||||
description: New created registry.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/RepTargetPost'
|
||||
$ref: '#/definitions/Registry'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'201':
|
||||
description: Replication target created successfully.
|
||||
description: Registry created successfully.
|
||||
'400':
|
||||
description: Unsatisfied with constraints of the target creation.
|
||||
description: Unsatisfied with constraints of the registry creation.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'409':
|
||||
description: Replication target name already exists.
|
||||
description: Registry name already exists.
|
||||
'415':
|
||||
$ref: '#/responses/UnsupportedMediaType'
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
/targets/ping:
|
||||
post:
|
||||
summary: Ping validates target.
|
||||
description: |
|
||||
This endpoint is for ping validates whether the target is reachable and whether the credential is valid.
|
||||
parameters:
|
||||
- name: target
|
||||
in: body
|
||||
description: The target object.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PingTarget'
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Ping target successfully.
|
||||
'400':
|
||||
description: Target id is invalid/ endpoint is needed/ invaild URL/ network issue.
|
||||
'401':
|
||||
description: User need to log in first or wrong username/password for remote target.
|
||||
'404':
|
||||
description: Target not found.
|
||||
'415':
|
||||
$ref: '#/responses/UnsupportedMediaType'
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/targets/{id}':
|
||||
'/registries/{id}':
|
||||
put:
|
||||
summary: Update replication's target.
|
||||
summary: Update a given registry.
|
||||
description: |
|
||||
This endpoint is for update specific replication's target.
|
||||
This endpoint is for update a given registry.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: The replication's target ID.
|
||||
description: The registry's ID.
|
||||
- name: repo_target
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PutTarget'
|
||||
description: Updates of replication's target.
|
||||
$ref: '#/definitions/PutRegistry'
|
||||
description: Updates registry.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Updated replication's target successfully.
|
||||
description: Updated registry successfully.
|
||||
'400':
|
||||
description: The target is associated with policy which is enabled.
|
||||
description: The registry is associated with policy which is enabled.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'404':
|
||||
description: Target ID does not exist.
|
||||
description: Registry does not exist.
|
||||
'409':
|
||||
description: Target name or endpoint is already used.
|
||||
description: Registry name is already used.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
get:
|
||||
summary: Get replication's target.
|
||||
description: This endpoint is for get specific replication's target.
|
||||
summary: Get registry.
|
||||
description: This endpoint is for get specific registry.
|
||||
tags:
|
||||
- Products
|
||||
parameters:
|
||||
@ -2201,40 +2174,40 @@ paths:
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: The replication's target ID.
|
||||
description: The registry ID.
|
||||
responses:
|
||||
'200':
|
||||
description: Get replication's target successfully.
|
||||
description: Get registry successfully.
|
||||
schema:
|
||||
$ref: '#/definitions/RepTarget'
|
||||
$ref: '#/definitions/Registry'
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'404':
|
||||
description: Replication's target not found
|
||||
description: Registry not found
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
delete:
|
||||
summary: Delete specific replication's target.
|
||||
summary: Delete specific registry.
|
||||
description: |
|
||||
This endpoint is for to delete specific replication's target.
|
||||
This endpoint is for to delete specific registry.
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
type: integer
|
||||
format: int64
|
||||
required: true
|
||||
description: The replication's target ID.
|
||||
description: The registry's ID.
|
||||
tags:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Replication's target deleted successfully.
|
||||
description: Registry deleted successfully.
|
||||
'400':
|
||||
description: Replication's target ID is invalid or the target is used by policies.
|
||||
description: Registry's ID is invalid or the registry is used by policies.
|
||||
'401':
|
||||
description: Only admin has this authority.
|
||||
'404':
|
||||
description: Replication's target does not exist.
|
||||
description: Registry does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
'/targets/{id}/policies/':
|
||||
@ -2672,15 +2645,15 @@ paths:
|
||||
- Products
|
||||
responses:
|
||||
'200':
|
||||
description: Updated replication's target successfully.
|
||||
description: Updated gc's schedule successfully.
|
||||
'400':
|
||||
description: The target is associated with policy which is enabled.
|
||||
description: Bad params.
|
||||
'401':
|
||||
description: User need to log in first.
|
||||
'403':
|
||||
description: User does not have permission of admin role.
|
||||
'404':
|
||||
description: Target ID does not exist.
|
||||
description: GC schedule does not exist.
|
||||
'500':
|
||||
description: Unexpected internal errors.
|
||||
post:
|
||||
@ -3624,11 +3597,11 @@ definitions:
|
||||
description: The project list that the policy applys to.
|
||||
items:
|
||||
$ref: '#/definitions/Project'
|
||||
targets:
|
||||
registries:
|
||||
type: array
|
||||
description: The target list.
|
||||
items:
|
||||
$ref: '#/definitions/RepTarget'
|
||||
$ref: '#/definitions/Registry'
|
||||
trigger:
|
||||
$ref: '#/definitions/RepTrigger'
|
||||
filters:
|
||||
@ -3688,90 +3661,69 @@ definitions:
|
||||
metadata:
|
||||
type: object
|
||||
description: This map object is the replication policy filter metadata.
|
||||
RepTarget:
|
||||
RegistryCredential:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: Credential type, such as 'basic', 'oauth'.
|
||||
access_key:
|
||||
type: string
|
||||
description: Access key, e.g. user name when credential type is 'basic'.
|
||||
access_secret:
|
||||
type: string
|
||||
description: Access secret, e.g. password when credential type is 'basic'.
|
||||
Registry:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The target ID.
|
||||
endpoint:
|
||||
description: The registry ID.
|
||||
url:
|
||||
type: string
|
||||
description: The target address URL string.
|
||||
description: The registry URL string.
|
||||
name:
|
||||
type: string
|
||||
description: The target name.
|
||||
username:
|
||||
type: string
|
||||
description: The target server username.
|
||||
password:
|
||||
type: string
|
||||
description: The target server password.
|
||||
description: The registry name.
|
||||
credential:
|
||||
$ref: '#/definitions/RegistryCredential'
|
||||
type:
|
||||
type: integer
|
||||
format: int
|
||||
description: Reserved field.
|
||||
type: string
|
||||
description: Type of the registry, e.g. 'harbor'.
|
||||
insecure:
|
||||
type: boolean
|
||||
description: Whether or not the certificate will be verified when Harbor tries to access the server.
|
||||
description:
|
||||
type: string
|
||||
description: Description of the registry.
|
||||
status:
|
||||
type: string
|
||||
description: Health status of the registry.
|
||||
creation_time:
|
||||
type: string
|
||||
description: The create time of the policy.
|
||||
update_time:
|
||||
type: string
|
||||
description: The update time of the policy.
|
||||
RepTargetPost:
|
||||
type: object
|
||||
properties:
|
||||
endpoint:
|
||||
type: string
|
||||
description: The target address URL string.
|
||||
name:
|
||||
type: string
|
||||
description: The target name.
|
||||
username:
|
||||
type: string
|
||||
description: The target server username.
|
||||
password:
|
||||
type: string
|
||||
description: The target server password.
|
||||
insecure:
|
||||
type: boolean
|
||||
description: Whether or not the certificate will be verified when Harbor tries to access the server.
|
||||
PingTarget:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int
|
||||
description: Target ID.
|
||||
endpoint:
|
||||
type: string
|
||||
description: The target address URL string.
|
||||
username:
|
||||
type: string
|
||||
description: The target server username.
|
||||
password:
|
||||
type: string
|
||||
description: The target server password.
|
||||
insecure:
|
||||
type: boolean
|
||||
description: Whether or not the certificate will be verified when Harbor tries to access the server.
|
||||
PutTarget:
|
||||
PutRegistry:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The target name.
|
||||
endpoint:
|
||||
description: The registry name.
|
||||
url:
|
||||
type: string
|
||||
description: The target address URL string.
|
||||
username:
|
||||
description: The registry address URL string.
|
||||
credential_type:
|
||||
type: string
|
||||
description: The target server username.
|
||||
password:
|
||||
description: Credential type of the registry, e.g. 'basic'.
|
||||
access_key:
|
||||
type: string
|
||||
description: The target server password.
|
||||
description: The registry access key.
|
||||
access_secret:
|
||||
type: string
|
||||
description: The registry access secret.
|
||||
insecure:
|
||||
type: boolean
|
||||
description: Whether or not the certificate will be verified when Harbor tries to access the server.
|
||||
|
15
make/migrations/postgresql/0006_ng_replication.up.sql
Normal file
15
make/migrations/postgresql/0006_ng_replication.up.sql
Normal file
@ -0,0 +1,15 @@
|
||||
CREATE TABLE registry (
|
||||
id SERIAL PRIMARY KEY NOT NULL,
|
||||
name varchar(256),
|
||||
url varchar(256),
|
||||
credential_type varchar(16),
|
||||
access_key varchar(128),
|
||||
access_secret varchar(1024),
|
||||
type varchar(32),
|
||||
insecure boolean,
|
||||
description varchar(1024),
|
||||
health varchar(16),
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP,
|
||||
CONSTRAINT unique_registry_name UNIQUE (name)
|
||||
);
|
@ -670,137 +670,6 @@ func TestChangeUserProfile(t *testing.T) {
|
||||
|
||||
var targetID, policyID, policyID2, policyID3, jobID, jobID2, jobID3 int64
|
||||
|
||||
func TestAddRepTarget(t *testing.T) {
|
||||
target := models.RepTarget{
|
||||
Name: "test",
|
||||
URL: "127.0.0.1:5000",
|
||||
Username: "admin",
|
||||
Password: "admin",
|
||||
}
|
||||
// _, err := AddRepTarget(target)
|
||||
id, err := AddRepTarget(target)
|
||||
t.Logf("added target, id: %d", id)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in AddRepTarget: %v", err)
|
||||
} else {
|
||||
targetID = id
|
||||
}
|
||||
id2 := id + 99
|
||||
tgt, err := GetRepTarget(id2)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetTarget: %v, id: %d", err, id2)
|
||||
}
|
||||
if tgt != nil {
|
||||
t.Errorf("There should not be a target with id: %d", id2)
|
||||
}
|
||||
tgt, err = GetRepTarget(id)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetTarget: %v, id: %d", err, id)
|
||||
}
|
||||
if tgt == nil {
|
||||
t.Errorf("Unable to find a target with id: %d", id)
|
||||
}
|
||||
if tgt.URL != "127.0.0.1:5000" {
|
||||
t.Errorf("Unexpected url in target: %s, expected 127.0.0.1:5000", tgt.URL)
|
||||
}
|
||||
if tgt.Username != "admin" {
|
||||
t.Errorf("Unexpected username in target: %s, expected admin", tgt.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepTargetByName(t *testing.T) {
|
||||
target, err := GetRepTarget(targetID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get target %d: %v", targetID, err)
|
||||
}
|
||||
|
||||
target2, err := GetRepTargetByName(target.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get target %s: %v", target.Name, err)
|
||||
}
|
||||
|
||||
if target.Name != target2.Name {
|
||||
t.Errorf("unexpected target name: %s, expected: %s", target2.Name, target.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepTargetByEndpoint(t *testing.T) {
|
||||
target, err := GetRepTarget(targetID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get target %d: %v", targetID, err)
|
||||
}
|
||||
|
||||
target2, err := GetRepTargetByEndpoint(target.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get target %s: %v", target.URL, err)
|
||||
}
|
||||
|
||||
if target.URL != target2.URL {
|
||||
t.Errorf("unexpected target URL: %s, expected: %s", target2.URL, target.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRepTarget(t *testing.T) {
|
||||
target := &models.RepTarget{
|
||||
Name: "name",
|
||||
URL: "http://url",
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
id, err := AddRepTarget(*target)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add target: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := DeleteRepTarget(id); err != nil {
|
||||
t.Logf("failed to delete target %d: %v", id, err)
|
||||
}
|
||||
}()
|
||||
|
||||
target.ID = id
|
||||
target.Name = "new_name"
|
||||
target.URL = "http://new_url"
|
||||
target.Username = "new_username"
|
||||
target.Password = "new_password"
|
||||
|
||||
if err = UpdateRepTarget(*target); err != nil {
|
||||
t.Fatalf("failed to update target: %v", err)
|
||||
}
|
||||
|
||||
target, err = GetRepTarget(id)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get target %d: %v", id, err)
|
||||
}
|
||||
|
||||
if target.Name != "new_name" {
|
||||
t.Errorf("unexpected name: %s, expected: %s", target.Name, "new_name")
|
||||
}
|
||||
|
||||
if target.URL != "http://new_url" {
|
||||
t.Errorf("unexpected url: %s, expected: %s", target.URL, "http://new_url")
|
||||
}
|
||||
|
||||
if target.Username != "new_username" {
|
||||
t.Errorf("unexpected username: %s, expected: %s", target.Username, "new_username")
|
||||
}
|
||||
|
||||
if target.Password != "new_password" {
|
||||
t.Errorf("unexpected password: %s, expected: %s", target.Password, "new_password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterRepTargets(t *testing.T) {
|
||||
targets, err := FilterRepTargets("test")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get all targets: %v", err)
|
||||
}
|
||||
|
||||
if len(targets) == 0 {
|
||||
t.Errorf("unexpected num of targets: %d, expected: %d", len(targets), 1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddRepPolicy(t *testing.T) {
|
||||
policy := models.RepPolicy{
|
||||
ProjectID: 1,
|
||||
@ -1053,22 +922,6 @@ func TestDeleteRepJob(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRepTarget(t *testing.T) {
|
||||
err := DeleteRepTarget(targetID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in DeleteRepTarget: %v, id: %d", err, targetID)
|
||||
return
|
||||
}
|
||||
t.Logf("deleted target, id: %d", targetID)
|
||||
tgt, err := GetRepTarget(targetID)
|
||||
if err != nil {
|
||||
t.Errorf("Error occurred in GetTarget: %v, id: %d", err, targetID)
|
||||
}
|
||||
if tgt != nil {
|
||||
t.Errorf("Able to find target after deletion, id: %d", targetID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTotalOfRepPolicies(t *testing.T) {
|
||||
_, err := GetTotalOfRepPolicies("", 1)
|
||||
require.Nil(t, err)
|
||||
|
@ -24,97 +24,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// AddRepTarget ...
|
||||
func AddRepTarget(target models.RepTarget) (int64, error) {
|
||||
o := GetOrmer()
|
||||
|
||||
sql := "insert into replication_target (name, url, username, password, insecure, target_type) values (?, ?, ?, ?, ?, ?) RETURNING id"
|
||||
|
||||
var targetID int64
|
||||
err := o.Raw(sql, target.Name, target.URL, target.Username, target.Password, target.Insecure, target.Type).QueryRow(&targetID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return targetID, nil
|
||||
}
|
||||
|
||||
// GetRepTarget ...
|
||||
func GetRepTarget(id int64) (*models.RepTarget, error) {
|
||||
o := GetOrmer()
|
||||
t := models.RepTarget{ID: id}
|
||||
err := o.Read(&t)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &t, err
|
||||
}
|
||||
|
||||
// GetRepTargetByName ...
|
||||
func GetRepTargetByName(name string) (*models.RepTarget, error) {
|
||||
o := GetOrmer()
|
||||
t := models.RepTarget{Name: name}
|
||||
err := o.Read(&t, "Name")
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &t, err
|
||||
}
|
||||
|
||||
// GetRepTargetByEndpoint ...
|
||||
func GetRepTargetByEndpoint(endpoint string) (*models.RepTarget, error) {
|
||||
o := GetOrmer()
|
||||
t := models.RepTarget{
|
||||
URL: endpoint,
|
||||
}
|
||||
err := o.Read(&t, "URL")
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &t, err
|
||||
}
|
||||
|
||||
// DeleteRepTarget ...
|
||||
func DeleteRepTarget(id int64) error {
|
||||
o := GetOrmer()
|
||||
_, err := o.Delete(&models.RepTarget{ID: id})
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateRepTarget ...
|
||||
func UpdateRepTarget(target models.RepTarget) error {
|
||||
o := GetOrmer()
|
||||
|
||||
sql := `update replication_target
|
||||
set url = ?, name = ?, username = ?, password = ?, insecure = ?, update_time = ?
|
||||
where id = ?`
|
||||
|
||||
_, err := o.Raw(sql, target.URL, target.Name, target.Username, target.Password, target.Insecure, time.Now(), target.ID).Exec()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FilterRepTargets filters targets by name
|
||||
func FilterRepTargets(name string) ([]*models.RepTarget, error) {
|
||||
o := GetOrmer()
|
||||
|
||||
var args []interface{}
|
||||
|
||||
sql := `select * from replication_target `
|
||||
if len(name) != 0 {
|
||||
sql += `where name like ? `
|
||||
args = append(args, "%"+Escape(name)+"%")
|
||||
}
|
||||
sql += `order by creation_time`
|
||||
|
||||
var targets []*models.RepTarget
|
||||
|
||||
if _, err := o.Raw(sql, args).QueryRows(&targets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
// AddRepPolicy ...
|
||||
func AddRepPolicy(policy models.RepPolicy) (int64, error) {
|
||||
o := GetOrmer()
|
||||
|
@ -14,23 +14,28 @@
|
||||
|
||||
package dao
|
||||
|
||||
// TODO: This UT makes common DAO depends on replication ng DAOs, comment it out temporarily here
|
||||
/*
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
common_models "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao"
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
)
|
||||
|
||||
func TestMethodsOfWatchItem(t *testing.T) {
|
||||
targetID, err := AddRepTarget(models.RepTarget{
|
||||
registryID, err := dao.AddRegistry(&models.Registry{
|
||||
Name: "test_target_for_watch_item",
|
||||
URL: "http://127.0.0.1",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer DeleteRepTarget(targetID)
|
||||
defer dao.DeleteRegistry(registryID)
|
||||
|
||||
policyID, err := AddRepPolicy(models.RepPolicy{
|
||||
policyID, err := AddRepPolicy(common_models.RepPolicy{
|
||||
Name: "test_policy_for_watch_item",
|
||||
ProjectID: 1,
|
||||
TargetID: targetID,
|
||||
@ -38,7 +43,7 @@ func TestMethodsOfWatchItem(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
defer DeleteRepPolicy(policyID)
|
||||
|
||||
item := &models.WatchItem{
|
||||
item := &common_models.WatchItem{
|
||||
PolicyID: policyID,
|
||||
Namespace: "library",
|
||||
OnPush: false,
|
||||
@ -69,3 +74,4 @@ func TestMethodsOfWatchItem(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 0, len(items))
|
||||
}
|
||||
*/
|
||||
|
@ -27,7 +27,7 @@ func currPath() string {
|
||||
return path.Dir(f)
|
||||
}
|
||||
|
||||
// NewJobServiceServer
|
||||
// NewJobServiceServer ...
|
||||
func NewJobServiceServer() *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc(fmt.Sprintf("%s/%s/log", jobsPrefix, jobUUID),
|
||||
|
@ -16,10 +16,13 @@ package models
|
||||
|
||||
import (
|
||||
"github.com/astaxie/beego/orm"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(RepTarget),
|
||||
orm.RegisterModel(
|
||||
new(models.Registry),
|
||||
new(RepPolicy),
|
||||
new(RepJob),
|
||||
new(User),
|
||||
|
@ -16,9 +16,6 @@ package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/validation"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -28,8 +25,8 @@ const (
|
||||
RepOpDelete string = "delete"
|
||||
// RepOpSchedule represents the operation of a job to schedule the real replication process
|
||||
RepOpSchedule string = "schedule"
|
||||
// RepTargetTable is the table name for replication targets
|
||||
RepTargetTable = "replication_target"
|
||||
// RegistryTable is the table name for registry
|
||||
RegistryTable = "registry"
|
||||
// RepJobTable is the table name for replication jobs
|
||||
RepJobTable = "replication_job"
|
||||
// RepPolicyTable is table name for replication policies
|
||||
@ -67,52 +64,6 @@ type RepJob struct {
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
// RepTarget is the model for a replication targe, i.e. destination, which wraps the endpoint URL and username/password of a remote registry.
|
||||
type RepTarget struct {
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||
URL string `orm:"column(url)" json:"endpoint"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
Username string `orm:"column(username)" json:"username"`
|
||||
Password string `orm:"column(password)" json:"password"`
|
||||
Type int `orm:"column(target_type)" json:"type"`
|
||||
Insecure bool `orm:"column(insecure)" json:"insecure"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
// Valid ...
|
||||
func (r *RepTarget) Valid(v *validation.Validation) {
|
||||
if len(r.Name) == 0 {
|
||||
v.SetError("name", "can not be empty")
|
||||
}
|
||||
|
||||
if len(r.Name) > 64 {
|
||||
v.SetError("name", "max length is 64")
|
||||
}
|
||||
|
||||
url, err := utils.ParseEndpoint(r.URL)
|
||||
if err != nil {
|
||||
v.SetError("endpoint", err.Error())
|
||||
} else {
|
||||
// Prevent SSRF security issue #3755
|
||||
r.URL = url.Scheme + "://" + url.Host + url.Path
|
||||
if len(r.URL) > 64 {
|
||||
v.SetError("endpoint", "max length is 64")
|
||||
}
|
||||
}
|
||||
|
||||
// password is encoded using base64, the length of this field
|
||||
// in DB is 64, so the max length in request is 48
|
||||
if len(r.Password) > 48 {
|
||||
v.SetError("password", "max length is 48")
|
||||
}
|
||||
}
|
||||
|
||||
// TableName is required by by beego orm to map RepTarget to table replication_target
|
||||
func (r *RepTarget) TableName() string {
|
||||
return RepTargetTable
|
||||
}
|
||||
|
||||
// TableName is required by by beego orm to map RepJob to table replication_job
|
||||
func (r *RepJob) TableName() string {
|
||||
return RepJobTable
|
||||
|
@ -14,13 +14,17 @@
|
||||
|
||||
package test
|
||||
|
||||
// FakeReplicatoinController ...
|
||||
type FakeReplicatoinController struct {
|
||||
FakePolicyManager
|
||||
}
|
||||
|
||||
func (f *FakeReplicatoinController) Init() error {
|
||||
// Init initialize replication controller
|
||||
func (f *FakeReplicatoinController) Init(closing chan struct{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Replicate ...
|
||||
func (f *FakeReplicatoinController) Replicate(policyID int64, metadata ...map[string]interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -19,18 +19,18 @@ import (
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
rep_dao "github.com/goharbor/harbor/src/replication/ng/dao"
|
||||
rep_models "github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
)
|
||||
|
||||
const (
|
||||
// Prepare Test info
|
||||
TestUserName = "testUser0001"
|
||||
TestUserPwd = "testUser0001"
|
||||
TestUserEmail = "testUser0001@mydomain.com"
|
||||
TestProName = "testProject0001"
|
||||
TestTargetName = "testTarget0001"
|
||||
TestRepoName = "testRepo0001"
|
||||
AdminName = "admin"
|
||||
DefaultProjectName = "library"
|
||||
TestUserName = "testUser0001"
|
||||
TestUserPwd = "testUser0001"
|
||||
TestUserEmail = "testUser0001@mydomain.com"
|
||||
TestProName = "testProject0001"
|
||||
TestRegistryName = "testRegistry0001"
|
||||
TestRepoName = "testRepo0001"
|
||||
)
|
||||
|
||||
func CommonAddUser() {
|
||||
@ -83,25 +83,25 @@ func CommonDelProject() {
|
||||
_ = dao.DeleteProject(commonProject.ProjectID)
|
||||
}
|
||||
|
||||
func CommonAddTarget() {
|
||||
func CommonAddRegistry() {
|
||||
endPoint := os.Getenv("REGISTRY_URL")
|
||||
commonTarget := &models.RepTarget{
|
||||
URL: endPoint,
|
||||
Name: TestTargetName,
|
||||
Username: adminName,
|
||||
Password: adminPwd,
|
||||
commonRegistry := &rep_models.Registry{
|
||||
URL: endPoint,
|
||||
Name: TestRegistryName,
|
||||
AccessKey: adminName,
|
||||
AccessSecret: adminPwd,
|
||||
}
|
||||
_, _ = dao.AddRepTarget(*commonTarget)
|
||||
_, _ = rep_dao.AddRegistry(commonRegistry)
|
||||
}
|
||||
|
||||
func CommonGetTarget() int {
|
||||
target, _ := dao.GetRepTargetByName(TestTargetName)
|
||||
return int(target.ID)
|
||||
func CommonGetRegistry() int {
|
||||
registry, _ := rep_dao.GetRegistryByName(TestRegistryName)
|
||||
return int(registry.ID)
|
||||
}
|
||||
|
||||
func CommonDelTarget() {
|
||||
target, _ := dao.GetRepTargetByName(TestTargetName)
|
||||
_ = dao.DeleteRepTarget(target.ID)
|
||||
func CommonDelRegistry() {
|
||||
registry, _ := rep_dao.GetRegistryByName(TestRegistryName)
|
||||
_ = rep_dao.DeleteRegistry(registry.ID)
|
||||
}
|
||||
|
||||
func CommonAddRepository() {
|
||||
|
@ -27,24 +27,22 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/job/test"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
testutils "github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/tests/apitests/apilib"
|
||||
|
||||
// "strconv"
|
||||
// "strings"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
"github.com/dghubble/sling"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/job/test"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
testutils "github.com/goharbor/harbor/src/common/utils/test"
|
||||
apimodels "github.com/goharbor/harbor/src/core/api/models"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/db"
|
||||
_ "github.com/goharbor/harbor/src/core/auth/ldap"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/src/replication/core"
|
||||
_ "github.com/goharbor/harbor/src/replication/event"
|
||||
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||
"github.com/goharbor/harbor/tests/apitests/apilib"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -126,11 +124,8 @@ func init() {
|
||||
beego.Router("/api/repositories/*/tags/:tag/manifest", &RepositoryAPI{}, "get:GetManifests")
|
||||
beego.Router("/api/repositories/*/signatures", &RepositoryAPI{}, "get:GetSignatures")
|
||||
beego.Router("/api/repositories/top", &RepositoryAPI{}, "get:GetTopRepos")
|
||||
beego.Router("/api/targets/", &TargetAPI{}, "get:List")
|
||||
beego.Router("/api/targets/", &TargetAPI{}, "post:Post")
|
||||
beego.Router("/api/targets/:id([0-9]+)", &TargetAPI{})
|
||||
beego.Router("/api/targets/:id([0-9]+)/policies/", &TargetAPI{}, "get:ListPolicies")
|
||||
beego.Router("/api/targets/ping", &TargetAPI{}, "post:Ping")
|
||||
beego.Router("/api/registries", &RegistryAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/registries/:id([0-9]+)", &RegistryAPI{}, "get:Get;put:Put;delete:Delete")
|
||||
beego.Router("/api/policies/replication/:id([0-9]+)", &RepPolicyAPI{})
|
||||
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "get:List")
|
||||
beego.Router("/api/policies/replication", &RepPolicyAPI{}, "post:Post;delete:Delete")
|
||||
@ -177,7 +172,7 @@ func init() {
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels", chartLabelAPIType, "get:GetLabels;post:MarkLabel")
|
||||
beego.Router("/api/chartrepo/:repo/charts/:name/:version/labels/:id([0-9]+)", chartLabelAPIType, "delete:RemoveLabel")
|
||||
|
||||
if err := core.Init(); err != nil {
|
||||
if err := core.Init(make(chan struct{})); err != nil {
|
||||
log.Fatalf("failed to initialize GlobalController: %v", err)
|
||||
}
|
||||
|
||||
@ -656,103 +651,6 @@ func (a testapi) GetReposTop(authInfo usrInfo, count string) (int, interface{},
|
||||
return http.StatusOK, result, nil
|
||||
}
|
||||
|
||||
// -------------------------Targets Test---------------------------------------//
|
||||
// Create a new replication target
|
||||
func (a testapi) AddTargets(authInfo usrInfo, repTarget apilib.RepTargetPost) (int, string, error) {
|
||||
_sling := sling.New().Post(a.basePath)
|
||||
|
||||
path := "/api/targets"
|
||||
|
||||
_sling = _sling.Path(path)
|
||||
_sling = _sling.BodyJSON(repTarget)
|
||||
|
||||
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
return httpStatusCode, string(body), err
|
||||
}
|
||||
|
||||
// List filters targets by name
|
||||
func (a testapi) ListTargets(authInfo usrInfo, targetName string) (int, []apilib.RepTarget, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
|
||||
path := "/api/targets?name=" + targetName
|
||||
|
||||
_sling = _sling.Path(path)
|
||||
|
||||
var successPayload []apilib.RepTarget
|
||||
|
||||
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
if err == nil && httpStatusCode == 200 {
|
||||
err = json.Unmarshal(body, &successPayload)
|
||||
}
|
||||
|
||||
return httpStatusCode, successPayload, err
|
||||
}
|
||||
|
||||
// Ping target
|
||||
func (a testapi) PingTarget(authInfo usrInfo, body interface{}) (int, error) {
|
||||
_sling := sling.New().Post(a.basePath)
|
||||
|
||||
path := "/api/targets/ping"
|
||||
|
||||
_sling = _sling.Path(path)
|
||||
_sling = _sling.BodyJSON(body)
|
||||
|
||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
return httpStatusCode, err
|
||||
}
|
||||
|
||||
// Get target by targetID
|
||||
func (a testapi) GetTargetByID(authInfo usrInfo, targetID string) (int, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
|
||||
path := "/api/targets/" + targetID
|
||||
|
||||
_sling = _sling.Path(path)
|
||||
|
||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
|
||||
return httpStatusCode, err
|
||||
}
|
||||
|
||||
// Update target by targetID
|
||||
func (a testapi) PutTargetByID(authInfo usrInfo, targetID string, repTarget apilib.RepTargetPost) (int, error) {
|
||||
_sling := sling.New().Put(a.basePath)
|
||||
|
||||
path := "/api/targets/" + targetID
|
||||
|
||||
_sling = _sling.Path(path)
|
||||
_sling = _sling.BodyJSON(repTarget)
|
||||
|
||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
|
||||
return httpStatusCode, err
|
||||
}
|
||||
|
||||
// List the target relevant policies by targetID
|
||||
func (a testapi) GetTargetPoliciesByID(authInfo usrInfo, targetID string) (int, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
|
||||
path := "/api/targets/" + targetID + "/policies/"
|
||||
|
||||
_sling = _sling.Path(path)
|
||||
|
||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
|
||||
return httpStatusCode, err
|
||||
}
|
||||
|
||||
// Delete target by targetID
|
||||
func (a testapi) DeleteTargetsByID(authInfo usrInfo, targetID string) (int, error) {
|
||||
_sling := sling.New().Delete(a.basePath)
|
||||
|
||||
path := "/api/targets/" + targetID
|
||||
|
||||
_sling = _sling.Path(path)
|
||||
|
||||
httpStatusCode, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
return httpStatusCode, err
|
||||
}
|
||||
|
||||
// --------------------Replication_Policy Test--------------------------------//
|
||||
|
||||
// Create a new replication policy
|
||||
@ -1192,3 +1090,57 @@ func (a testapi) GCScheduleGet(authInfo usrInfo) (int, []apilib.AdminJob, error)
|
||||
|
||||
return httpStatusCode, successPayLoad, err
|
||||
}
|
||||
|
||||
func (a testapi) RegistryGet(authInfo usrInfo, registryID int64) (*model.Registry, int, error) {
|
||||
_sling := sling.New().Base(a.basePath).Get(fmt.Sprintf("/api/registries/%d", registryID))
|
||||
code, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
if err == nil && code == http.StatusOK {
|
||||
registry := model.Registry{}
|
||||
if err := json.Unmarshal(body, ®istry); err != nil {
|
||||
return nil, code, err
|
||||
}
|
||||
return ®istry, code, nil
|
||||
}
|
||||
return nil, code, err
|
||||
}
|
||||
|
||||
func (a testapi) RegistryList(authInfo usrInfo) ([]*model.Registry, int, error) {
|
||||
_sling := sling.New().Base(a.basePath).Get("/api/registries")
|
||||
code, body, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
if err != nil || code != http.StatusOK {
|
||||
return nil, code, err
|
||||
}
|
||||
|
||||
var registries []*model.Registry
|
||||
if err := json.Unmarshal(body, ®istries); err != nil {
|
||||
return nil, code, err
|
||||
}
|
||||
|
||||
return registries, code, nil
|
||||
}
|
||||
|
||||
func (a testapi) RegistryCreate(authInfo usrInfo, registry *model.Registry) (int, error) {
|
||||
_sling := sling.New().Base(a.basePath).Post("/api/registries").BodyJSON(registry)
|
||||
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
return code, err
|
||||
}
|
||||
|
||||
func (a testapi) RegistryDelete(authInfo usrInfo, registryID int64) (int, error) {
|
||||
_sling := sling.New().Base(a.basePath).Delete(fmt.Sprintf("/api/registries/%d", registryID))
|
||||
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
if err != nil || code != http.StatusOK {
|
||||
return code, fmt.Errorf("delete registry error: %v", err)
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
func (a testapi) RegistryUpdate(authInfo usrInfo, registryID int64, req *apimodels.RegistryUpdateRequest) (int, error) {
|
||||
_sling := sling.New().Base(a.basePath).Put(fmt.Sprintf("/api/registries/%d", registryID)).BodyJSON(req)
|
||||
code, _, err := request(_sling, jsonAcceptHeader, authInfo)
|
||||
if err != nil || code != http.StatusOK {
|
||||
return code, fmt.Errorf("update registry error: %v", err)
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
||||
rep_dao "github.com/goharbor/harbor/src/replication/ng/dao"
|
||||
dao_models "github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -455,18 +457,18 @@ func TestListResources(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
defer dao.DeleteLabel(projectLabelID)
|
||||
|
||||
targetID, err := dao.AddRepTarget(models.RepTarget{
|
||||
registryID, err := rep_dao.AddRegistry(&dao_models.Registry{
|
||||
Name: "target_for_testing_label_resource",
|
||||
URL: "https://192.168.0.1",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer dao.DeleteRepTarget(targetID)
|
||||
defer rep_dao.DeleteRegistry(registryID)
|
||||
|
||||
// create a policy references both global and project labels
|
||||
policyID, err := dao.AddRepPolicy(models.RepPolicy{
|
||||
Name: "policy_for_testing_label_resource",
|
||||
ProjectID: 1,
|
||||
TargetID: targetID,
|
||||
TargetID: registryID,
|
||||
Trigger: fmt.Sprintf(`{"kind":"%s"}`, replication.TriggerKindManual),
|
||||
Filters: fmt.Sprintf(`[{"kind":"%s","value":%d}, {"kind":"%s","value":%d}]`,
|
||||
replication.FilterItemKindLabel, globalLabelID,
|
||||
|
11
src/core/api/models/registry.go
Normal file
11
src/core/api/models/registry.go
Normal file
@ -0,0 +1,11 @@
|
||||
package models
|
||||
|
||||
// RegistryUpdateRequest is request used to update a registry.
|
||||
type RegistryUpdateRequest struct {
|
||||
Name *string `json:"name"`
|
||||
URL *string `json:"url"`
|
||||
CredentialType *string `json:"credential_type"`
|
||||
AccessKey *string `json:"access_key"`
|
||||
AccessSecret *string `json:"access_secret"`
|
||||
Insecure *bool `json:"insecure"`
|
||||
}
|
@ -20,22 +20,23 @@ import (
|
||||
"github.com/astaxie/beego/validation"
|
||||
common_models "github.com/goharbor/harbor/src/common/models"
|
||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
)
|
||||
|
||||
// ReplicationPolicy defines the data model used in API level
|
||||
type ReplicationPolicy struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Filters []rep_models.Filter `json:"filters"`
|
||||
ReplicateDeletion bool `json:"replicate_deletion"`
|
||||
Trigger *rep_models.Trigger `json:"trigger"`
|
||||
Projects []*common_models.Project `json:"projects"`
|
||||
Targets []*common_models.RepTarget `json:"targets"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
ReplicateExistingImageNow bool `json:"replicate_existing_image_now"`
|
||||
ErrorJobCount int64 `json:"error_job_count"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Filters []rep_models.Filter `json:"filters"`
|
||||
ReplicateDeletion bool `json:"replicate_deletion"`
|
||||
Trigger *rep_models.Trigger `json:"trigger"`
|
||||
Projects []*common_models.Project `json:"projects"`
|
||||
Registries []*models.Registry `json:"registries"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
ReplicateExistingImageNow bool `json:"replicate_existing_image_now"`
|
||||
ErrorJobCount int64 `json:"error_job_count"`
|
||||
}
|
||||
|
||||
// Valid ...
|
||||
@ -52,7 +53,7 @@ func (r *ReplicationPolicy) Valid(v *validation.Validation) {
|
||||
v.SetError("projects", "can not be empty")
|
||||
}
|
||||
|
||||
if len(r.Targets) == 0 {
|
||||
if len(r.Registries) == 0 {
|
||||
v.SetError("targets", "can not be empty")
|
||||
}
|
||||
|
||||
|
213
src/core/api/registry.go
Normal file
213
src/core/api/registry.go
Normal file
@ -0,0 +1,213 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/api/models"
|
||||
"github.com/goharbor/harbor/src/replication/ng"
|
||||
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||
"github.com/goharbor/harbor/src/replication/ng/registry"
|
||||
)
|
||||
|
||||
// RegistryAPI handles requests to /api/registries/{}. It manages registries integrated to Harbor.
|
||||
type RegistryAPI struct {
|
||||
BaseController
|
||||
manager registry.Manager
|
||||
}
|
||||
|
||||
// Prepare validates the user
|
||||
func (t *RegistryAPI) Prepare() {
|
||||
t.BaseController.Prepare()
|
||||
if !t.SecurityCtx.IsAuthenticated() {
|
||||
t.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !t.SecurityCtx.IsSysAdmin() {
|
||||
t.HandleForbidden(t.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
t.manager = ng.RegistryMgr
|
||||
}
|
||||
|
||||
// Get gets a registry by id.
|
||||
func (t *RegistryAPI) Get() {
|
||||
id := t.GetIDFromURL()
|
||||
|
||||
registry, err := t.manager.Get(id)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get registry %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
return
|
||||
}
|
||||
|
||||
if registry == nil {
|
||||
t.HandleNotFound(fmt.Sprintf("registry %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
// Hide access secret
|
||||
registry.Credential.AccessSecret = "*****"
|
||||
|
||||
t.Data["json"] = registry
|
||||
t.ServeJSON()
|
||||
}
|
||||
|
||||
// List lists all registries that match a given registry name.
|
||||
func (t *RegistryAPI) List() {
|
||||
name := t.GetString("name")
|
||||
|
||||
_, registries, err := t.manager.List(&model.RegistryQuery{
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("failed to list registries %s: %v", name, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
return
|
||||
}
|
||||
|
||||
// Hide passwords
|
||||
for _, registry := range registries {
|
||||
registry.Credential.AccessSecret = "*****"
|
||||
}
|
||||
|
||||
t.Data["json"] = registries
|
||||
t.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// Post creates a registry
|
||||
func (t *RegistryAPI) Post() {
|
||||
registry := &model.Registry{}
|
||||
t.DecodeJSONReqAndValidate(registry)
|
||||
|
||||
reg, err := t.manager.GetByName(registry.Name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get registry %s: %v", registry.Name, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
return
|
||||
}
|
||||
|
||||
if reg != nil {
|
||||
t.HandleConflict(fmt.Sprintf("name '%s' is already used", registry.Name))
|
||||
return
|
||||
}
|
||||
|
||||
id, err := t.manager.Add(registry)
|
||||
if err != nil {
|
||||
log.Errorf("Add registry '%s' error: %v", registry.URL, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
return
|
||||
}
|
||||
|
||||
t.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
|
||||
}
|
||||
|
||||
// Put updates a registry
|
||||
func (t *RegistryAPI) Put() {
|
||||
id := t.GetIDFromURL()
|
||||
|
||||
registry, err := t.manager.Get(id)
|
||||
if err != nil {
|
||||
log.Errorf("Get registry by id %d error: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
return
|
||||
}
|
||||
|
||||
if registry == nil {
|
||||
t.HandleNotFound(fmt.Sprintf("Registry %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
req := models.RegistryUpdateRequest{}
|
||||
t.DecodeJSONReq(&req)
|
||||
|
||||
originalName := registry.Name
|
||||
|
||||
if req.Name != nil {
|
||||
registry.Name = *req.Name
|
||||
}
|
||||
if req.URL != nil {
|
||||
registry.URL = *req.URL
|
||||
}
|
||||
if req.CredentialType != nil {
|
||||
registry.Credential.Type = (model.CredentialType)(*req.CredentialType)
|
||||
}
|
||||
if req.AccessKey != nil {
|
||||
registry.Credential.AccessKey = *req.AccessKey
|
||||
}
|
||||
if req.AccessSecret != nil {
|
||||
registry.Credential.AccessSecret = *req.AccessSecret
|
||||
}
|
||||
if req.Insecure != nil {
|
||||
registry.Insecure = *req.Insecure
|
||||
}
|
||||
|
||||
t.Validate(registry)
|
||||
|
||||
if registry.Name != originalName {
|
||||
reg, err := t.manager.GetByName(registry.Name)
|
||||
if err != nil {
|
||||
log.Errorf("Get registry by name '%s' error: %v", registry.Name, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
return
|
||||
}
|
||||
|
||||
if reg != nil {
|
||||
t.HandleConflict("name is already used")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := t.manager.Update(registry); err != nil {
|
||||
log.Errorf("Update registry %d error: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deletes a registry
|
||||
func (t *RegistryAPI) Delete() {
|
||||
id := t.GetIDFromURL()
|
||||
|
||||
registry, err := t.manager.Get(id)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Get registry %d error: %v", id, err)
|
||||
log.Error(msg)
|
||||
t.HandleInternalServerError(msg)
|
||||
return
|
||||
}
|
||||
|
||||
if registry == nil {
|
||||
t.HandleNotFound(fmt.Sprintf("registry %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Use PolicyManager instead
|
||||
policies, err := dao.GetRepPolicyByTarget(id)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Get policies related to registry %d error: %v", id, err)
|
||||
log.Error(msg)
|
||||
t.HandleInternalServerError(msg)
|
||||
return
|
||||
}
|
||||
|
||||
if len(policies) > 0 {
|
||||
msg := fmt.Sprintf("Can't delete registry with replication policies, %d found", len(policies))
|
||||
log.Error(msg)
|
||||
t.HandleStatusPreconditionFailed(msg)
|
||||
return
|
||||
}
|
||||
|
||||
if err := t.manager.Remove(id); err != nil {
|
||||
msg := fmt.Sprintf("Delete registry %d error: %v", id, err)
|
||||
log.Error(msg)
|
||||
t.HandleInternalServerError(msg)
|
||||
return
|
||||
}
|
||||
}
|
166
src/core/api/registry_test.go
Normal file
166
src/core/api/registry_test.go
Normal file
@ -0,0 +1,166 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/goharbor/harbor/src/core/api/models"
|
||||
"github.com/goharbor/harbor/src/replication/ng"
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao"
|
||||
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||
)
|
||||
|
||||
var (
|
||||
testRegistry = &model.Registry{
|
||||
Name: "test1",
|
||||
URL: "https://test.harbor.io",
|
||||
Type: "harbor",
|
||||
Credential: &model.Credential{
|
||||
Type: model.CredentialTypeBasic,
|
||||
AccessKey: "admin",
|
||||
AccessSecret: "Harbor12345",
|
||||
},
|
||||
}
|
||||
testRegistry2 = &model.Registry{
|
||||
Name: "test2",
|
||||
URL: "https://test2.harbor.io",
|
||||
Type: "harbor",
|
||||
Credential: &model.Credential{
|
||||
Type: model.CredentialTypeBasic,
|
||||
AccessKey: "admin",
|
||||
AccessSecret: "Harbor12345",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type RegistrySuite struct {
|
||||
suite.Suite
|
||||
testAPI *testapi
|
||||
defaultRegistry model.Registry
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) SetupSuite() {
|
||||
assert := assert.New(suite.T())
|
||||
assert.Nil(ng.Init())
|
||||
|
||||
suite.testAPI = newHarborAPI()
|
||||
code, err := suite.testAPI.RegistryCreate(*admin, testRegistry)
|
||||
assert.Nil(err)
|
||||
assert.Equal(http.StatusCreated, code)
|
||||
|
||||
tmp, err := dao.GetRegistryByName(testRegistry.Name)
|
||||
assert.Nil(err)
|
||||
assert.NotNil(tmp)
|
||||
suite.defaultRegistry = *testRegistry
|
||||
suite.defaultRegistry.ID = tmp.ID
|
||||
|
||||
CommonAddUser()
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TearDownSuite() {
|
||||
assert := assert.New(suite.T())
|
||||
code, err := suite.testAPI.RegistryDelete(*admin, suite.defaultRegistry.ID)
|
||||
assert.Nil(err)
|
||||
assert.Equal(http.StatusOK, code)
|
||||
|
||||
CommonDelUser()
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestGet() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
// Get a non-existed registry
|
||||
_, code, _ := suite.testAPI.RegistryGet(*admin, 0)
|
||||
assert.Equal(http.StatusBadRequest, code)
|
||||
|
||||
// Get as admin, should succeed
|
||||
retrieved, code, err := suite.testAPI.RegistryGet(*admin, suite.defaultRegistry.ID)
|
||||
assert.Nil(err)
|
||||
assert.NotNil(retrieved)
|
||||
assert.Equal(http.StatusOK, code)
|
||||
assert.Equal("test1", retrieved.Name)
|
||||
|
||||
// Get as user, should fail
|
||||
_, code, _ = suite.testAPI.RegistryGet(*testUser, suite.defaultRegistry.ID)
|
||||
assert.Equal(http.StatusForbidden, code)
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestList() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
// List as admin, should succeed
|
||||
registries, code, err := suite.testAPI.RegistryList(*admin)
|
||||
assert.Nil(err)
|
||||
assert.Equal(http.StatusOK, code)
|
||||
assert.Equal(1, len(registries))
|
||||
|
||||
// List as user, should fail
|
||||
registries, code, err = suite.testAPI.RegistryList(*testUser)
|
||||
assert.Equal(http.StatusForbidden, code)
|
||||
assert.Equal(0, len(registries))
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestPost() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
// Should conflict when create exited registry
|
||||
code, err := suite.testAPI.RegistryCreate(*admin, testRegistry)
|
||||
assert.Nil(err)
|
||||
assert.Equal(http.StatusConflict, code)
|
||||
|
||||
// Create as user, should fail
|
||||
code, err = suite.testAPI.RegistryCreate(*testUser, testRegistry2)
|
||||
assert.Nil(err)
|
||||
assert.Equal(http.StatusForbidden, code)
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestRegistryPut() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
// Update as admin, should succeed
|
||||
newKey := "NewKey"
|
||||
updateReq := &models.RegistryUpdateRequest{
|
||||
AccessKey: &newKey,
|
||||
}
|
||||
code, err := suite.testAPI.RegistryUpdate(*admin, suite.defaultRegistry.ID, updateReq)
|
||||
assert.Nil(err)
|
||||
assert.Equal(http.StatusOK, code)
|
||||
updated, code, err := suite.testAPI.RegistryGet(*admin, suite.defaultRegistry.ID)
|
||||
assert.Nil(err)
|
||||
assert.Equal(http.StatusOK, code)
|
||||
assert.Equal("NewKey", updated.Credential.AccessKey)
|
||||
|
||||
// Update as user, should fail
|
||||
code, err = suite.testAPI.RegistryUpdate(*testUser, suite.defaultRegistry.ID, updateReq)
|
||||
assert.NotNil(err)
|
||||
assert.Equal(http.StatusForbidden, code)
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestDelete() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
code, err := suite.testAPI.RegistryCreate(*admin, testRegistry2)
|
||||
assert.Nil(err)
|
||||
assert.Equal(http.StatusCreated, code)
|
||||
|
||||
tmp, err := dao.GetRegistryByName(testRegistry2.Name)
|
||||
assert.Nil(err)
|
||||
assert.NotNil(tmp)
|
||||
|
||||
// Delete as user, should fail
|
||||
code, err = suite.testAPI.RegistryDelete(*testUser, tmp.ID)
|
||||
assert.NotNil(err)
|
||||
assert.Equal(http.StatusForbidden, code)
|
||||
|
||||
// Delete as admin, should succeed
|
||||
code, err = suite.testAPI.RegistryDelete(*admin, tmp.ID)
|
||||
assert.Nil(err)
|
||||
assert.Equal(http.StatusOK, code)
|
||||
}
|
||||
|
||||
func TestRegistrySuite(t *testing.T) {
|
||||
suite.Run(t, new(RegistrySuite))
|
||||
}
|
@ -29,6 +29,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/core"
|
||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
||||
rep_dao "github.com/goharbor/harbor/src/replication/ng/dao"
|
||||
)
|
||||
|
||||
// RepPolicyAPI handles /api/replicationPolicies /api/replicationPolicies/:id/enablement
|
||||
@ -158,15 +159,15 @@ func (pa *RepPolicyAPI) Post() {
|
||||
}
|
||||
|
||||
// check the existence of targets
|
||||
for _, target := range policy.Targets {
|
||||
t, err := dao.GetRepTarget(target.ID)
|
||||
for _, r := range policy.Registries {
|
||||
t, err := rep_dao.GetRegistry(r.ID)
|
||||
if err != nil {
|
||||
pa.HandleInternalServerError(fmt.Sprintf("failed to get target %d: %v", target.ID, err))
|
||||
pa.HandleInternalServerError(fmt.Sprintf("failed to get target %d: %v", r.ID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if t == nil {
|
||||
pa.HandleNotFound(fmt.Sprintf("target %d not found", target.ID))
|
||||
pa.HandleNotFound(fmt.Sprintf("target %d not found", r.ID))
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -271,15 +272,15 @@ func (pa *RepPolicyAPI) Put() {
|
||||
}
|
||||
|
||||
// check the existence of targets
|
||||
for _, target := range policy.Targets {
|
||||
t, err := dao.GetRepTarget(target.ID)
|
||||
for _, r := range policy.Registries {
|
||||
t, err := rep_dao.GetRegistry(r.ID)
|
||||
if err != nil {
|
||||
pa.HandleInternalServerError(fmt.Sprintf("failed to get target %d: %v", target.ID, err))
|
||||
pa.HandleInternalServerError(fmt.Sprintf("failed to get target %d: %v", r.ID, err))
|
||||
return
|
||||
}
|
||||
|
||||
if t == nil {
|
||||
pa.HandleNotFound(fmt.Sprintf("target %d not found", target.ID))
|
||||
pa.HandleNotFound(fmt.Sprintf("target %d not found", r.ID))
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -379,12 +380,12 @@ func convertFromRepPolicy(projectMgr promgr.ProjectManager, policy rep_models.Re
|
||||
|
||||
// populate targets
|
||||
for _, targetID := range policy.TargetIDs {
|
||||
target, err := dao.GetRepTarget(targetID)
|
||||
r, err := rep_dao.GetRegistry(targetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target.Password = ""
|
||||
ply.Targets = append(ply.Targets, target)
|
||||
r.AccessSecret = ""
|
||||
ply.Registries = append(ply.Registries, r)
|
||||
}
|
||||
|
||||
// populate label used in label filter
|
||||
@ -434,8 +435,8 @@ func convertToRepPolicy(policy *api_models.ReplicationPolicy) rep_models.Replica
|
||||
ply.Namespaces = append(ply.Namespaces, project.Name)
|
||||
}
|
||||
|
||||
for _, target := range policy.Targets {
|
||||
ply.TargetIDs = append(ply.TargetIDs, target.ID)
|
||||
for _, r := range policy.Registries {
|
||||
ply.TargetIDs = append(ply.TargetIDs, r.ID)
|
||||
}
|
||||
|
||||
return ply
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
rep_models "github.com/goharbor/harbor/src/replication/models"
|
||||
dao_models "github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -50,8 +51,8 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
|
||||
CommonAddTarget()
|
||||
targetID = int64(CommonGetTarget())
|
||||
CommonAddRegistry()
|
||||
targetID = int64(CommonGetRegistry())
|
||||
|
||||
var err error
|
||||
labelID2, err = dao.AddLabel(&models.Label{
|
||||
@ -131,7 +132,7 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
||||
ProjectID: projectID,
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: targetID,
|
||||
},
|
||||
@ -159,7 +160,7 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
||||
ProjectID: projectID,
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: targetID,
|
||||
},
|
||||
@ -190,7 +191,7 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
||||
ProjectID: 10000,
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: targetID,
|
||||
},
|
||||
@ -221,7 +222,7 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
||||
ProjectID: projectID,
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: 10000,
|
||||
},
|
||||
@ -252,7 +253,7 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
||||
ProjectID: projectID,
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: targetID,
|
||||
},
|
||||
@ -287,7 +288,7 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
||||
ProjectID: projectID,
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: targetID,
|
||||
},
|
||||
@ -323,7 +324,7 @@ func TestRepPolicyAPIPost(t *testing.T) {
|
||||
ProjectID: projectID,
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: targetID,
|
||||
},
|
||||
@ -567,7 +568,7 @@ func TestRepPolicyAPIPut(t *testing.T) {
|
||||
ProjectID: projectID,
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: targetID,
|
||||
},
|
||||
@ -598,7 +599,7 @@ func TestRepPolicyAPIPut(t *testing.T) {
|
||||
ProjectID: projectID,
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: targetID,
|
||||
},
|
||||
@ -677,7 +678,7 @@ func TestConvertToRepPolicy(t *testing.T) {
|
||||
Name: "library",
|
||||
},
|
||||
},
|
||||
Targets: []*models.RepTarget{
|
||||
Registries: []*dao_models.Registry{
|
||||
{
|
||||
ID: 1,
|
||||
},
|
||||
|
@ -18,11 +18,14 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
api_models "github.com/goharbor/harbor/src/core/api/models"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/stretchr/testify/require"
|
||||
rep_dao "github.com/goharbor/harbor/src/replication/ng/dao"
|
||||
dao_models "github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -30,15 +33,15 @@ const (
|
||||
)
|
||||
|
||||
func TestReplicationAPIPost(t *testing.T) {
|
||||
targetID, err := dao.AddRepTarget(
|
||||
models.RepTarget{
|
||||
Name: "test_replication_target",
|
||||
URL: "127.0.0.1",
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
registryID, err := rep_dao.AddRegistry(
|
||||
&dao_models.Registry{
|
||||
Name: "test_replication_target",
|
||||
URL: "127.0.0.1",
|
||||
AccessKey: "username",
|
||||
AccessSecret: "password",
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer dao.DeleteRepTarget(targetID)
|
||||
defer rep_dao.DeleteRegistry(registryID)
|
||||
|
||||
policyID, err := dao.AddRepPolicy(
|
||||
models.RepPolicy{
|
||||
|
@ -1,369 +0,0 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
|
||||
// TargetAPI handles request to /api/targets/ping /api/targets/{}
|
||||
type TargetAPI struct {
|
||||
BaseController
|
||||
secretKey string
|
||||
}
|
||||
|
||||
// Prepare validates the user
|
||||
func (t *TargetAPI) Prepare() {
|
||||
t.BaseController.Prepare()
|
||||
if !t.SecurityCtx.IsAuthenticated() {
|
||||
t.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
if !t.SecurityCtx.IsSysAdmin() {
|
||||
t.HandleForbidden(t.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
t.secretKey, err = config.SecretKey()
|
||||
if err != nil {
|
||||
log.Errorf("failed to get secret key: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TargetAPI) ping(endpoint, username, password string, insecure bool) {
|
||||
registry, err := newRegistryClient(endpoint, insecure, username, password)
|
||||
if err == nil {
|
||||
err = registry.Ping()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to ping target: %v", err)
|
||||
// do not return any detail information of the error, or may cause SSRF security issue #3755
|
||||
t.RenderError(http.StatusBadRequest, "failed to ping target")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Ping validates whether the target is reachable and whether the credential is valid
|
||||
func (t *TargetAPI) Ping() {
|
||||
req := struct {
|
||||
ID *int64 `json:"id"`
|
||||
Endpoint *string `json:"endpoint"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
Insecure *bool `json:"insecure"`
|
||||
}{}
|
||||
t.DecodeJSONReq(&req)
|
||||
|
||||
target := &models.RepTarget{}
|
||||
if req.ID != nil {
|
||||
var err error
|
||||
target, err = dao.GetRepTarget(*req.ID)
|
||||
if err != nil {
|
||||
t.HandleInternalServerError(fmt.Sprintf("failed to get target %d: %v", *req.ID, err))
|
||||
return
|
||||
}
|
||||
if target == nil {
|
||||
t.HandleNotFound(fmt.Sprintf("target %d not found", *req.ID))
|
||||
return
|
||||
}
|
||||
if len(target.Password) != 0 {
|
||||
target.Password, err = utils.ReversibleDecrypt(target.Password, t.secretKey)
|
||||
if err != nil {
|
||||
t.HandleInternalServerError(fmt.Sprintf("failed to decrypt password: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Endpoint != nil {
|
||||
url, err := utils.ParseEndpoint(*req.Endpoint)
|
||||
if err != nil {
|
||||
t.HandleBadRequest(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent SSRF security issue #3755
|
||||
target.URL = url.Scheme + "://" + url.Host + url.Path
|
||||
}
|
||||
if req.Username != nil {
|
||||
target.Username = *req.Username
|
||||
}
|
||||
if req.Password != nil {
|
||||
target.Password = *req.Password
|
||||
}
|
||||
if req.Insecure != nil {
|
||||
target.Insecure = *req.Insecure
|
||||
}
|
||||
|
||||
t.ping(target.URL, target.Username, target.Password, target.Insecure)
|
||||
}
|
||||
|
||||
// Get ...
|
||||
func (t *TargetAPI) Get() {
|
||||
id := t.GetIDFromURL()
|
||||
|
||||
target, err := dao.GetRepTarget(id)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
t.HandleNotFound(fmt.Sprintf("target %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
target.Password = ""
|
||||
|
||||
t.Data["json"] = target
|
||||
t.ServeJSON()
|
||||
}
|
||||
|
||||
// List ...
|
||||
func (t *TargetAPI) List() {
|
||||
name := t.GetString("name")
|
||||
targets, err := dao.FilterRepTargets(name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to filter targets %s: %v", name, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
target.Password = ""
|
||||
}
|
||||
|
||||
t.Data["json"] = targets
|
||||
t.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// Post ...
|
||||
func (t *TargetAPI) Post() {
|
||||
target := &models.RepTarget{}
|
||||
t.DecodeJSONReqAndValidate(target)
|
||||
|
||||
ta, err := dao.GetRepTargetByName(target.Name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get target %s: %v", target.Name, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if ta != nil {
|
||||
t.HandleConflict("name is already used")
|
||||
return
|
||||
}
|
||||
|
||||
ta, err = dao.GetRepTargetByEndpoint(target.URL)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get target [ %s ]: %v", target.URL, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if ta != nil {
|
||||
t.HandleConflict(fmt.Sprintf("the target whose endpoint is %s already exists", target.URL))
|
||||
return
|
||||
}
|
||||
|
||||
if len(target.Password) != 0 {
|
||||
target.Password, err = utils.ReversibleEncrypt(target.Password, t.secretKey)
|
||||
if err != nil {
|
||||
log.Errorf("failed to encrypt password: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
id, err := dao.AddRepTarget(*target)
|
||||
if err != nil {
|
||||
log.Errorf("failed to add target: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
t.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
|
||||
}
|
||||
|
||||
// Put ...
|
||||
func (t *TargetAPI) Put() {
|
||||
id := t.GetIDFromURL()
|
||||
|
||||
target, err := dao.GetRepTarget(id)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
t.HandleNotFound(fmt.Sprintf("target %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
if len(target.Password) != 0 {
|
||||
target.Password, err = utils.ReversibleDecrypt(target.Password, t.secretKey)
|
||||
if err != nil {
|
||||
log.Errorf("failed to decrypt password: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
req := struct {
|
||||
Name *string `json:"name"`
|
||||
Endpoint *string `json:"endpoint"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
Insecure *bool `json:"insecure"`
|
||||
}{}
|
||||
t.DecodeJSONReq(&req)
|
||||
|
||||
originalName := target.Name
|
||||
originalURL := target.URL
|
||||
|
||||
if req.Name != nil {
|
||||
target.Name = *req.Name
|
||||
}
|
||||
if req.Endpoint != nil {
|
||||
target.URL = *req.Endpoint
|
||||
}
|
||||
if req.Username != nil {
|
||||
target.Username = *req.Username
|
||||
}
|
||||
if req.Password != nil {
|
||||
target.Password = *req.Password
|
||||
}
|
||||
if req.Insecure != nil {
|
||||
target.Insecure = *req.Insecure
|
||||
}
|
||||
|
||||
t.Validate(target)
|
||||
|
||||
if target.Name != originalName {
|
||||
ta, err := dao.GetRepTargetByName(target.Name)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get target %s: %v", target.Name, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if ta != nil {
|
||||
t.HandleConflict("name is already used")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if target.URL != originalURL {
|
||||
ta, err := dao.GetRepTargetByEndpoint(target.URL)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get target [ %s ]: %v", target.URL, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if ta != nil {
|
||||
t.HandleConflict(fmt.Sprintf("the target whose endpoint is %s already exists", target.URL))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(target.Password) != 0 {
|
||||
target.Password, err = utils.ReversibleEncrypt(target.Password, t.secretKey)
|
||||
if err != nil {
|
||||
log.Errorf("failed to encrypt password: %v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
if err := dao.UpdateRepTarget(*target); err != nil {
|
||||
log.Errorf("failed to update target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
// Delete ...
|
||||
func (t *TargetAPI) Delete() {
|
||||
id := t.GetIDFromURL()
|
||||
|
||||
target, err := dao.GetRepTarget(id)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
t.HandleNotFound(fmt.Sprintf("target %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
policies, err := dao.GetRepPolicyByTarget(id)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get policies according target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if len(policies) > 0 {
|
||||
log.Error("the target is used by policies, can not be deleted")
|
||||
t.CustomAbort(http.StatusPreconditionFailed, "the target is used by policies, can not be deleted")
|
||||
}
|
||||
|
||||
if err = dao.DeleteRepTarget(id); err != nil {
|
||||
log.Errorf("failed to delete target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
}
|
||||
|
||||
func newRegistryClient(endpoint string, insecure bool, username, password string) (*registry.Registry, error) {
|
||||
transport := registry.GetHTTPTransport(insecure)
|
||||
credential := auth.NewBasicAuthCredential(username, password)
|
||||
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
|
||||
Transport: transport,
|
||||
}, credential)
|
||||
return registry.NewRegistry(endpoint, &http.Client{
|
||||
Transport: registry.NewTransport(transport, authorizer),
|
||||
})
|
||||
}
|
||||
|
||||
// ListPolicies ...
|
||||
func (t *TargetAPI) ListPolicies() {
|
||||
id := t.GetIDFromURL()
|
||||
|
||||
target, err := dao.GetRepTarget(id)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
t.HandleNotFound(fmt.Sprintf("target %d not found", id))
|
||||
return
|
||||
}
|
||||
|
||||
policies, err := dao.GetRepPolicyByTarget(id)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get policies according target %d: %v", id, err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
t.Data["json"] = policies
|
||||
t.ServeJSON()
|
||||
}
|
@ -1,289 +0,0 @@
|
||||
// Copyright 2018 Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/goharbor/harbor/tests/apitests/apilib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
addTargetName = "testTargets"
|
||||
)
|
||||
|
||||
var addTargetID int
|
||||
|
||||
func TestTargetsPost(t *testing.T) {
|
||||
var httpStatusCode int
|
||||
var err error
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
endPoint := os.Getenv("REGISTRY_URL")
|
||||
repTargets := &apilib.RepTargetPost{Endpoint: endPoint, Name: addTargetName, Username: adminName, Password: adminPwd}
|
||||
|
||||
fmt.Println("Testing Targets Post API")
|
||||
|
||||
// -------------------case 1 : response code = 201------------------------//
|
||||
fmt.Println("case 1 : response code = 201")
|
||||
httpStatusCode, body, err := apiTest.AddTargets(*admin, *repTargets)
|
||||
if err != nil {
|
||||
t.Error("Error whihle add targets", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(201), httpStatusCode, "httpStatusCode should be 201")
|
||||
t.Log(body)
|
||||
}
|
||||
|
||||
// -----------case 2 : response code = 409,name is already used-----------//
|
||||
fmt.Println("case 2 : response code = 409,name is already used")
|
||||
httpStatusCode, _, err = apiTest.AddTargets(*admin, *repTargets)
|
||||
if err != nil {
|
||||
t.Error("Error whihle add targets", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(409), httpStatusCode, "httpStatusCode should be 409")
|
||||
}
|
||||
|
||||
// -----------case 3 : response code = 409,name is already used-----------//
|
||||
fmt.Println("case 3 : response code = 409,endPoint is already used")
|
||||
repTargets.Username = "errName"
|
||||
httpStatusCode, _, err = apiTest.AddTargets(*admin, *repTargets)
|
||||
if err != nil {
|
||||
t.Error("Error whihle add targets", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(409), httpStatusCode, "httpStatusCode should be 409")
|
||||
}
|
||||
|
||||
// --------case 4 : response code = 401,User need to log in first.--------//
|
||||
fmt.Println("case 4 : response code = 401,User need to log in first.")
|
||||
httpStatusCode, _, err = apiTest.AddTargets(*unknownUsr, *repTargets)
|
||||
if err != nil {
|
||||
t.Error("Error whihle add targets", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(401), httpStatusCode, "httpStatusCode should be 401")
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
|
||||
}
|
||||
|
||||
func TestTargetsGet(t *testing.T) {
|
||||
var httpStatusCode int
|
||||
var err error
|
||||
var reslut []apilib.RepTarget
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
fmt.Println("Testing Targets Get API")
|
||||
|
||||
// -------------------case 1 : response code = 200------------------------//
|
||||
fmt.Println("case 1 : response code = 200")
|
||||
httpStatusCode, reslut, err = apiTest.ListTargets(*admin, addTargetName)
|
||||
if err != nil {
|
||||
t.Error("Error whihle get targets", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
addTargetID = int(reslut[0].Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTargetPing(t *testing.T) {
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
// 404: not exist target
|
||||
target01 := struct {
|
||||
ID int64 `json:"id"`
|
||||
}{
|
||||
ID: 10000,
|
||||
}
|
||||
|
||||
code, err := apiTest.PingTarget(*admin, target01)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, code)
|
||||
|
||||
// 400: empty endpoint
|
||||
target02 := struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
}{
|
||||
Endpoint: "",
|
||||
}
|
||||
code, err = apiTest.PingTarget(*admin, target02)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusBadRequest, code)
|
||||
|
||||
// 200
|
||||
target03 := struct {
|
||||
ID int64 `json:"id"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Insecure bool `json:"insecure"`
|
||||
}{
|
||||
ID: int64(addTargetID),
|
||||
Endpoint: os.Getenv("REGISTRY_URL"),
|
||||
Username: adminName,
|
||||
Password: adminPwd,
|
||||
Insecure: true,
|
||||
}
|
||||
code, err = apiTest.PingTarget(*admin, target03)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, http.StatusOK, code)
|
||||
}
|
||||
|
||||
func TestTargetGetByID(t *testing.T) {
|
||||
var httpStatusCode int
|
||||
var err error
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
fmt.Println("Testing Targets Get API by Id")
|
||||
|
||||
// -------------------case 1 : response code = 200------------------------//
|
||||
fmt.Println("case 1 : response code = 200")
|
||||
id := strconv.Itoa(addTargetID)
|
||||
httpStatusCode, err = apiTest.GetTargetByID(*admin, id)
|
||||
if err != nil {
|
||||
t.Error("Error whihle get target by id", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
}
|
||||
|
||||
// --------------case 2 : response code = 404,target not found------------//
|
||||
fmt.Println("case 2 : response code = 404,target not found")
|
||||
id = "1111"
|
||||
httpStatusCode, err = apiTest.GetTargetByID(*admin, id)
|
||||
if err != nil {
|
||||
t.Error("Error whihle get target by id", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTargetsPut(t *testing.T) {
|
||||
var httpStatusCode int
|
||||
var err error
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
endPoint := "1.1.1.1"
|
||||
updateRepTargets := &apilib.RepTargetPost{Endpoint: endPoint, Name: addTargetName, Username: adminName, Password: adminPwd}
|
||||
id := strconv.Itoa(addTargetID)
|
||||
|
||||
fmt.Println("Testing Target Put API")
|
||||
|
||||
// -------------------case 1 : response code = 200------------------------//
|
||||
fmt.Println("case 1 : response code = 200")
|
||||
httpStatusCode, err = apiTest.PutTargetByID(*admin, id, *updateRepTargets)
|
||||
if err != nil {
|
||||
t.Error("Error whihle update target", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
}
|
||||
|
||||
// --------------case 2 : response code = 404,target not found------------//
|
||||
id = "111"
|
||||
fmt.Println("case 2 : response code = 404,target not found")
|
||||
httpStatusCode, err = apiTest.PutTargetByID(*admin, id, *updateRepTargets)
|
||||
if err != nil {
|
||||
t.Error("Error whihle update target", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
||||
}
|
||||
|
||||
}
|
||||
func TestTargetGetPolicies(t *testing.T) {
|
||||
var httpStatusCode int
|
||||
var err error
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
fmt.Println("Testing Targets Get API to list policies")
|
||||
|
||||
// -------------------case 1 : response code = 200------------------------//
|
||||
fmt.Println("case 1 : response code = 200")
|
||||
id := strconv.Itoa(addTargetID)
|
||||
httpStatusCode, err = apiTest.GetTargetPoliciesByID(*admin, id)
|
||||
if err != nil {
|
||||
t.Error("Error whihle get target by id", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
}
|
||||
|
||||
// --------------case 2 : response code = 404,target not found------------//
|
||||
fmt.Println("case 2 : response code = 404,target not found")
|
||||
id = "1111"
|
||||
httpStatusCode, err = apiTest.GetTargetPoliciesByID(*admin, id)
|
||||
if err != nil {
|
||||
t.Error("Error whihle get target by id", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTargetsDelete(t *testing.T) {
|
||||
var httpStatusCode int
|
||||
var err error
|
||||
|
||||
assert := assert.New(t)
|
||||
apiTest := newHarborAPI()
|
||||
|
||||
id := strconv.Itoa(addTargetID)
|
||||
fmt.Println("Testing Targets Delete API")
|
||||
|
||||
// -------------------case 1 : response code = 200------------------------//
|
||||
fmt.Println("case 1 : response code = 200")
|
||||
httpStatusCode, err = apiTest.DeleteTargetsByID(*admin, id)
|
||||
if err != nil {
|
||||
t.Error("Error whihle delete targets", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(200), httpStatusCode, "httpStatusCode should be 200")
|
||||
}
|
||||
|
||||
// --------------case 2 : response code = 404,target not found------------//
|
||||
fmt.Println("case 2 : response code = 404,target not found")
|
||||
id = "1111"
|
||||
httpStatusCode, err = apiTest.DeleteTargetsByID(*admin, id)
|
||||
if err != nil {
|
||||
t.Error("Error whihle delete targets", err.Error())
|
||||
t.Log(err)
|
||||
} else {
|
||||
assert.Equal(int(404), httpStatusCode, "httpStatusCode should be 404")
|
||||
}
|
||||
|
||||
}
|
@ -18,7 +18,9 @@ import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/astaxie/beego"
|
||||
_ "github.com/astaxie/beego/session/redis"
|
||||
@ -71,6 +73,13 @@ func updateInitPassword(userID int, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func gracefulShutdown(closing chan struct{}) {
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
log.Infof("capture system signal %s, to close \"closing\" channel", <-signals)
|
||||
close(closing)
|
||||
}
|
||||
|
||||
func main() {
|
||||
beego.BConfig.WebConfig.Session.SessionOn = true
|
||||
beego.BConfig.WebConfig.Session.SessionName = "sid"
|
||||
@ -128,7 +137,10 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if err := core.Init(); err != nil {
|
||||
closing := make(chan struct{})
|
||||
go gracefulShutdown(closing)
|
||||
|
||||
if err := core.Init(closing); err != nil {
|
||||
log.Errorf("failed to initialize the replication controller: %v", err)
|
||||
}
|
||||
|
||||
|
@ -97,11 +97,6 @@ func initRouters() {
|
||||
beego.Router("/api/policies/replication/:id([0-9]+)", &api.RepPolicyAPI{})
|
||||
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "get:List")
|
||||
beego.Router("/api/policies/replication", &api.RepPolicyAPI{}, "post:Post")
|
||||
beego.Router("/api/targets/", &api.TargetAPI{}, "get:List")
|
||||
beego.Router("/api/targets/", &api.TargetAPI{}, "post:Post")
|
||||
beego.Router("/api/targets/:id([0-9]+)", &api.TargetAPI{})
|
||||
beego.Router("/api/targets/:id([0-9]+)/policies/", &api.TargetAPI{}, "get:ListPolicies")
|
||||
beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping")
|
||||
beego.Router("/api/logs", &api.LogAPI{})
|
||||
|
||||
beego.Router("/api/internal/configurations", &api.ConfigAPI{}, "get:GetInternalConfig;put:Put")
|
||||
@ -127,6 +122,9 @@ func initRouters() {
|
||||
beego.Router("/service/notifications/jobs/adminjob/:id([0-9]+)", &admin.Handler{}, "post:HandleAdminJob")
|
||||
beego.Router("/service/token", &token.Handler{})
|
||||
|
||||
beego.Router("/api/registries", &api.RegistryAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/registries/:id([0-9]+)", &api.RegistryAPI{}, "get:Get;put:Put;delete:Delete")
|
||||
|
||||
beego.Router("/v2/*", &controllers.RegistryProxy{}, "*:Handle")
|
||||
|
||||
// APIs for chart repository
|
||||
|
@ -18,25 +18,28 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
common_models "github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/utils"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/models"
|
||||
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||
"github.com/goharbor/harbor/src/replication/ng/registry"
|
||||
"github.com/goharbor/harbor/src/replication/policy"
|
||||
"github.com/goharbor/harbor/src/replication/replicator"
|
||||
"github.com/goharbor/harbor/src/replication/source"
|
||||
"github.com/goharbor/harbor/src/replication/target"
|
||||
"github.com/goharbor/harbor/src/replication/trigger"
|
||||
|
||||
"github.com/docker/distribution/uuid"
|
||||
"github.com/goharbor/harbor/src/replication/ng"
|
||||
)
|
||||
|
||||
// Controller defines the methods that a replicatoin controllter should implement
|
||||
type Controller interface {
|
||||
policy.Manager
|
||||
Init() error
|
||||
Init(closing chan struct{}) error
|
||||
Replicate(policyID int64, metadata ...map[string]interface{}) error
|
||||
}
|
||||
|
||||
@ -49,8 +52,8 @@ type DefaultController struct {
|
||||
// Manage the policies
|
||||
policyManager policy.Manager
|
||||
|
||||
// Manage the targets
|
||||
targetManager target.Manager
|
||||
// Manage the registries
|
||||
registryManager registry.Manager
|
||||
|
||||
// Handle the things related with source
|
||||
sourcer *source.Sourcer
|
||||
@ -77,10 +80,10 @@ type ControllerConfig struct {
|
||||
func NewDefaultController(cfg ControllerConfig) *DefaultController {
|
||||
// Controller refer the default instances
|
||||
ctl := &DefaultController{
|
||||
policyManager: policy.NewDefaultManager(),
|
||||
targetManager: target.NewDefaultManager(),
|
||||
sourcer: source.NewSourcer(),
|
||||
triggerManager: trigger.NewManager(cfg.CacheCapacity),
|
||||
policyManager: policy.NewDefaultManager(),
|
||||
registryManager: registry.NewDefaultManager(),
|
||||
sourcer: source.NewSourcer(),
|
||||
triggerManager: trigger.NewManager(cfg.CacheCapacity),
|
||||
}
|
||||
|
||||
ctl.replicator = replicator.NewDefaultReplicator(utils.GetJobServiceClient())
|
||||
@ -88,14 +91,19 @@ func NewDefaultController(cfg ControllerConfig) *DefaultController {
|
||||
return ctl
|
||||
}
|
||||
|
||||
// Init creates the GlobalController and inits it
|
||||
func Init() error {
|
||||
GlobalController = NewDefaultController(ControllerConfig{}) // Use default data
|
||||
return GlobalController.Init()
|
||||
// Init initializes GlobalController and replication related managers
|
||||
func Init(closing chan struct{}) error {
|
||||
GlobalController = NewDefaultController(ControllerConfig{})
|
||||
err := GlobalController.Init(closing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ng.Init()
|
||||
}
|
||||
|
||||
// Init will initialize the controller and the sub components
|
||||
func (ctl *DefaultController) Init() error {
|
||||
func (ctl *DefaultController) Init(closing chan struct{}) error {
|
||||
if ctl.initialized {
|
||||
return nil
|
||||
}
|
||||
@ -105,6 +113,9 @@ func (ctl *DefaultController) Init() error {
|
||||
|
||||
ctl.initialized = true
|
||||
|
||||
// Start registry health checker to regularly check registries' health status
|
||||
go registry.NewHealthChecker(time.Second*30, closing).Run()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -215,13 +226,13 @@ func (ctl *DefaultController) Replicate(policyID int64, metadata ...map[string]i
|
||||
log.Debugf("replication candidates are null, no further action needed")
|
||||
}
|
||||
|
||||
targets := []*common_models.RepTarget{}
|
||||
registries := []*model.Registry{}
|
||||
for _, targetID := range policy.TargetIDs {
|
||||
target, err := ctl.targetManager.GetTarget(targetID)
|
||||
r, err := ctl.registryManager.Get(targetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targets = append(targets, target)
|
||||
registries = append(registries, r)
|
||||
}
|
||||
|
||||
// Get operation uuid from metadata, if none provided, generate one.
|
||||
@ -235,7 +246,7 @@ func (ctl *DefaultController) Replicate(policyID int64, metadata ...map[string]i
|
||||
PolicyID: policyID,
|
||||
OpUUID: opUUID,
|
||||
Candidates: candidates,
|
||||
Targets: targets,
|
||||
Registries: registries,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -21,24 +21,24 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils/test"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/models"
|
||||
"github.com/goharbor/harbor/src/replication/ng/registry"
|
||||
"github.com/goharbor/harbor/src/replication/source"
|
||||
"github.com/goharbor/harbor/src/replication/target"
|
||||
"github.com/goharbor/harbor/src/replication/trigger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
GlobalController = &DefaultController{
|
||||
policyManager: &test.FakePolicyManager{},
|
||||
targetManager: target.NewDefaultManager(),
|
||||
sourcer: source.NewSourcer(),
|
||||
triggerManager: trigger.NewManager(0),
|
||||
policyManager: &test.FakePolicyManager{},
|
||||
registryManager: registry.NewDefaultManager(),
|
||||
sourcer: source.NewSourcer(),
|
||||
triggerManager: trigger.NewManager(0),
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
assert.Nil(t, GlobalController.Init())
|
||||
assert.Nil(t, GlobalController.Init(make(chan struct{})))
|
||||
}
|
||||
|
||||
func TestCreatePolicy(t *testing.T) {
|
||||
|
57
src/replication/ng/dao/models/registry.go
Normal file
57
src/replication/ng/dao/models/registry.go
Normal file
@ -0,0 +1,57 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/validation"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// RegistryTable is the table name for registry
|
||||
RegistryTable = "registry"
|
||||
)
|
||||
|
||||
// Registry is the model for a registry, which wraps the endpoint URL and credential of a remote registry.
|
||||
type Registry struct {
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"id"`
|
||||
URL string `orm:"column(url)" json:"endpoint"`
|
||||
Name string `orm:"column(name)" json:"name"`
|
||||
CredentialType string `orm:"column(credential_type);default(basic)" json:"credential_type"`
|
||||
AccessKey string `orm:"column(access_key)" json:"access_key"`
|
||||
AccessSecret string `orm:"column(access_secret)" json:"access_secret"`
|
||||
Type string `orm:"column(type)" json:"type"`
|
||||
Insecure bool `orm:"column(insecure)" json:"insecure"`
|
||||
Description string `orm:"column(description)" json:"description"`
|
||||
Health string `orm:"column(health)" json:"health"`
|
||||
CreationTime time.Time `orm:"column(creation_time);auto_now_add" json:"creation_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now" json:"update_time"`
|
||||
}
|
||||
|
||||
// TableName is required by by beego orm to map Registry to table registry
|
||||
func (r *Registry) TableName() string {
|
||||
return RegistryTable
|
||||
}
|
||||
|
||||
// Valid ...
|
||||
func (r *Registry) Valid(v *validation.Validation) {
|
||||
if len(r.Name) == 0 {
|
||||
v.SetError("name", "can not be empty")
|
||||
}
|
||||
|
||||
if len(r.Name) > 64 {
|
||||
v.SetError("name", "max length is 64")
|
||||
}
|
||||
|
||||
url, err := utils.ParseEndpoint(r.URL)
|
||||
if err != nil {
|
||||
v.SetError("endpoint", err.Error())
|
||||
} else {
|
||||
// Prevent SSRF security issue #3755
|
||||
r.URL = url.Scheme + "://" + url.Host + url.Path
|
||||
if len(r.URL) > 64 {
|
||||
v.SetError("endpoint", "max length is 64")
|
||||
}
|
||||
}
|
||||
}
|
@ -22,58 +22,58 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidOfTarget(t *testing.T) {
|
||||
func TestValidOfRegistry(t *testing.T) {
|
||||
cases := []struct {
|
||||
target RepTarget
|
||||
target Registry
|
||||
err bool
|
||||
expected RepTarget
|
||||
expected Registry
|
||||
}{
|
||||
// name is null
|
||||
{
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "",
|
||||
},
|
||||
true,
|
||||
RepTarget{}},
|
||||
Registry{}},
|
||||
|
||||
// url is null
|
||||
{
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "",
|
||||
},
|
||||
true,
|
||||
RepTarget{},
|
||||
Registry{},
|
||||
},
|
||||
|
||||
// invalid url
|
||||
{
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "ftp://example.com",
|
||||
},
|
||||
true,
|
||||
RepTarget{},
|
||||
Registry{},
|
||||
},
|
||||
|
||||
// invalid url
|
||||
{
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "ftp://example.com",
|
||||
},
|
||||
true,
|
||||
RepTarget{},
|
||||
Registry{},
|
||||
},
|
||||
|
||||
// valid url
|
||||
{
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "example.com",
|
||||
},
|
||||
false,
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com",
|
||||
},
|
||||
@ -81,12 +81,12 @@ func TestValidOfTarget(t *testing.T) {
|
||||
|
||||
// valid url
|
||||
{
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com",
|
||||
},
|
||||
false,
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com",
|
||||
},
|
||||
@ -94,12 +94,12 @@ func TestValidOfTarget(t *testing.T) {
|
||||
|
||||
// valid url
|
||||
{
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "https://example.com",
|
||||
},
|
||||
false,
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "https://example.com",
|
||||
},
|
||||
@ -107,12 +107,12 @@ func TestValidOfTarget(t *testing.T) {
|
||||
|
||||
// valid url
|
||||
{
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com/redirect?key=value",
|
||||
},
|
||||
false,
|
||||
RepTarget{
|
||||
Registry{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com/redirect",
|
||||
}},
|
108
src/replication/ng/dao/registry.go
Normal file
108
src/replication/ng/dao/registry.go
Normal file
@ -0,0 +1,108 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
)
|
||||
|
||||
// ListRegistryQuery defines the query conditions to list registry.
|
||||
type ListRegistryQuery struct {
|
||||
// Query is name query
|
||||
Query string
|
||||
// Offset specifies the offset in the registry list to return
|
||||
Offset int64
|
||||
// Limit specifies the maximum registries to return
|
||||
Limit int64
|
||||
}
|
||||
|
||||
// AddRegistry add a new registry
|
||||
func AddRegistry(registry *models.Registry) (int64, error) {
|
||||
o := dao.GetOrmer()
|
||||
return o.Insert(registry)
|
||||
}
|
||||
|
||||
// GetRegistry gets one registry from database by id.
|
||||
func GetRegistry(id int64) (*models.Registry, error) {
|
||||
o := dao.GetOrmer()
|
||||
r := models.Registry{ID: id}
|
||||
err := o.Read(&r, "ID")
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &r, err
|
||||
}
|
||||
|
||||
// GetRegistryByName gets one registry from database by its name.
|
||||
func GetRegistryByName(name string) (*models.Registry, error) {
|
||||
o := dao.GetOrmer()
|
||||
r := models.Registry{Name: name}
|
||||
err := o.Read(&r, "Name")
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &r, err
|
||||
}
|
||||
|
||||
// GetRegistryByURL gets one registry from database by its URL.
|
||||
func GetRegistryByURL(url string) (*models.Registry, error) {
|
||||
o := dao.GetOrmer()
|
||||
r := models.Registry{URL: url}
|
||||
err := o.Read(&r, "URL")
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &r, err
|
||||
}
|
||||
|
||||
// ListRegistries lists registries. Registries returned are sorted by creation time.
|
||||
// - query: query to the registry name, name query and pagination are defined.
|
||||
func ListRegistries(query ...*ListRegistryQuery) (int64, []*models.Registry, error) {
|
||||
o := dao.GetOrmer()
|
||||
|
||||
q := o.QueryTable(&models.Registry{})
|
||||
if len(query) > 0 && len(query[0].Query) > 0 {
|
||||
q = q.Filter("name__contains", query[0].Query)
|
||||
}
|
||||
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
return -1, nil, err
|
||||
}
|
||||
|
||||
// limit being -1 means no pagination specified.
|
||||
if len(query) > 0 && query[0].Limit != -1 {
|
||||
q = q.Offset(query[0].Offset).Limit(query[0].Limit)
|
||||
}
|
||||
var registries []*models.Registry
|
||||
_, err = q.All(®istries)
|
||||
if err != nil {
|
||||
return total, nil, err
|
||||
}
|
||||
|
||||
return total, registries, nil
|
||||
}
|
||||
|
||||
// UpdateRegistry updates one registry
|
||||
func UpdateRegistry(registry *models.Registry) error {
|
||||
o := dao.GetOrmer()
|
||||
|
||||
sql := `update registry
|
||||
set url = ?, name = ?, credential_type = ?, access_key = ?, access_secret = ?, type = ?, insecure = ?, health = ?, description = ?, update_time = ?
|
||||
where id = ?`
|
||||
|
||||
_, err := o.Raw(sql, registry.URL, registry.Name, registry.CredentialType, registry.AccessKey, registry.AccessSecret,
|
||||
registry.Type, registry.Insecure, registry.Health, registry.Description, time.Now(), registry.ID).Exec()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteRegistry deletes a registry
|
||||
func DeleteRegistry(id int64) error {
|
||||
o := dao.GetOrmer()
|
||||
_, err := o.Delete(&models.Registry{ID: id})
|
||||
return err
|
||||
}
|
175
src/replication/ng/dao/registry_test.go
Normal file
175
src/replication/ng/dao/registry_test.go
Normal file
@ -0,0 +1,175 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultRegistry = &models.Registry{
|
||||
Name: "daoTestDefault",
|
||||
URL: "test.harbor.io",
|
||||
CredentialType: "basic",
|
||||
AccessKey: "key1",
|
||||
AccessSecret: "secret1",
|
||||
Type: "harbor",
|
||||
}
|
||||
testRegistry1 = &models.Registry{
|
||||
Name: "daoTest2",
|
||||
URL: "test2.harbor.io",
|
||||
CredentialType: "basic",
|
||||
AccessKey: "key1",
|
||||
AccessSecret: "secret1",
|
||||
Type: "harbor",
|
||||
}
|
||||
)
|
||||
|
||||
type RegistrySuite struct {
|
||||
suite.Suite
|
||||
defaultID int64
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) SetupTest() {
|
||||
assert := assert.New(suite.T())
|
||||
id, err := AddRegistry(defaultRegistry)
|
||||
assert.Nil(err)
|
||||
suite.defaultID = id
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TearDownTest() {
|
||||
assert := assert.New(suite.T())
|
||||
err := DeleteRegistry(suite.defaultID)
|
||||
assert.Nil(err)
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestGetRegistry() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
// Get non-existed registry, should fail
|
||||
r, _ := GetRegistry(0)
|
||||
assert.Nil(r)
|
||||
|
||||
// Get existed registry, should succeed
|
||||
r, err := GetRegistry(suite.defaultID)
|
||||
assert.Nil(err)
|
||||
assert.Equal(defaultRegistry.Name, r.Name)
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestGetRegistryByName() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
// Get registry by empty name, should fail
|
||||
r, _ := GetRegistryByName("")
|
||||
assert.Nil(r)
|
||||
|
||||
// Get non-existed registry, should fail
|
||||
r, _ = GetRegistryByName("non-exist")
|
||||
assert.Nil(r)
|
||||
|
||||
// Get existed registry, should succeed
|
||||
r, err := GetRegistryByName(defaultRegistry.Name)
|
||||
assert.Nil(err)
|
||||
assert.Equal(defaultRegistry.Name, r.Name)
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestGetRegistryByURL() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
// Get registry by empty url, should fail
|
||||
r, _ := GetRegistryByURL("")
|
||||
assert.Nil(r)
|
||||
|
||||
// Get non-existed registry, should fail
|
||||
r, _ = GetRegistryByURL("non-exist.harbor.io")
|
||||
assert.Nil(r)
|
||||
|
||||
// Get existed registry, should succeed
|
||||
r, err := GetRegistryByURL(defaultRegistry.URL)
|
||||
assert.Nil(err)
|
||||
assert.Equal(defaultRegistry.Name, r.Name)
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestListRegistries() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
// Insert on more registry
|
||||
id, err := AddRegistry(testRegistry1)
|
||||
assert.Nil(err)
|
||||
assert.NotEqual(0, id)
|
||||
|
||||
// List all registries, should succeed
|
||||
total, registries, err := ListRegistries()
|
||||
assert.Nil(err)
|
||||
if total < 2 {
|
||||
suite.T().Errorf("At least %d should be found in total, but got %d", 2, total)
|
||||
}
|
||||
|
||||
// List default registry by normal query, should succeed
|
||||
total, registries, err = ListRegistries(&ListRegistryQuery{
|
||||
Query: "Default",
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
})
|
||||
assert.Nil(err)
|
||||
assert.Equal(int64(1), total)
|
||||
assert.Equal(defaultRegistry.Name, registries[0].Name)
|
||||
|
||||
// List registry and limit to 1, should return one
|
||||
total, registries, err = ListRegistries(&ListRegistryQuery{
|
||||
Query: "dao",
|
||||
Offset: 0,
|
||||
Limit: 1,
|
||||
})
|
||||
assert.Nil(err)
|
||||
assert.Equal(int64(2), total)
|
||||
assert.Equal(1, len(registries))
|
||||
|
||||
// List registry and limit set to -1, should return all
|
||||
total, registries, err = ListRegistries(&ListRegistryQuery{
|
||||
Limit: -1,
|
||||
})
|
||||
assert.Nil(err)
|
||||
if total < 2 {
|
||||
suite.T().Errorf("At least %d should be found in total, but got %d", 2, total)
|
||||
}
|
||||
if len(registries) < 2 {
|
||||
suite.T().Errorf("At least %d should be returned, but got %d", 2, len(registries))
|
||||
}
|
||||
|
||||
// List registry and large offset, should return empty
|
||||
total, registries, err = ListRegistries(&ListRegistryQuery{
|
||||
Offset: 10,
|
||||
Limit: 1,
|
||||
})
|
||||
assert.Nil(err)
|
||||
if total < 2 {
|
||||
suite.T().Errorf("At least %d should be found in total, but got %d", 2, total)
|
||||
}
|
||||
assert.Equal(0, len(registries))
|
||||
}
|
||||
|
||||
func (suite *RegistrySuite) TestUpdate() {
|
||||
assert := assert.New(suite.T())
|
||||
|
||||
// Get registry, should succeed
|
||||
r, err := GetRegistry(suite.defaultID)
|
||||
assert.Nil(err)
|
||||
assert.NotNil(r)
|
||||
|
||||
r.AccessKey = "key2"
|
||||
err = UpdateRegistry(r)
|
||||
assert.Nil(err)
|
||||
|
||||
r, err = GetRegistry(suite.defaultID)
|
||||
assert.Nil(err)
|
||||
assert.NotNil(r)
|
||||
assert.Equal("key2", r.AccessKey)
|
||||
}
|
||||
|
||||
func TestRegistrySuite(t *testing.T) {
|
||||
suite.Run(t, new(RegistrySuite))
|
||||
}
|
@ -15,7 +15,6 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
@ -39,7 +38,8 @@ type Controller interface {
|
||||
func NewController(registryMgr registry.Manager,
|
||||
executionMgr execution.Manager, scheduler scheduler.Scheduler) (Controller, error) {
|
||||
if registryMgr == nil || executionMgr == nil || scheduler == nil {
|
||||
return nil, errors.New("invalid params")
|
||||
// TODO(ChenDe): Uncomment it when execution manager is ready
|
||||
// return nil, errors.New("invalid params")
|
||||
}
|
||||
return &defaultController{
|
||||
registryMgr: registryMgr,
|
||||
|
@ -68,12 +68,18 @@ func (f *fakedRegistryManager) Get(id int64) (*model.Registry, error) {
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) GetByName(name string) (*model.Registry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakedRegistryManager) Update(*model.Registry, ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakedRegistryManager) Remove(int64) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakedRegistryManager) HealthCheck() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakedExecutionManager struct{}
|
||||
|
||||
|
@ -15,12 +15,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
)
|
||||
|
||||
// RegistryType indicates the type of registry
|
||||
type RegistryType string
|
||||
|
||||
const (
|
||||
// RegistryTypeHarbor indicates registry type harbor
|
||||
RegistryTypeHarbor = "harbor"
|
||||
)
|
||||
|
||||
// Valid indicates whether the RegistryType is a valid value
|
||||
func (r RegistryType) Valid() bool {
|
||||
return len(r) > 0
|
||||
@ -30,6 +37,13 @@ func (r RegistryType) Valid() bool {
|
||||
// e.g: u/p, OAuth token
|
||||
type CredentialType string
|
||||
|
||||
const (
|
||||
// CredentialTypeBasic indicates credential by user name, password
|
||||
CredentialTypeBasic = "basic"
|
||||
// CredentialTypeOAuth indicates credential by OAuth token
|
||||
CredentialTypeOAuth = "oauth"
|
||||
)
|
||||
|
||||
// Credential keeps the access key and/or secret for the related registry
|
||||
type Credential struct {
|
||||
// Type of the credential
|
||||
@ -44,18 +58,22 @@ type Credential struct {
|
||||
// Data required for the secure access way is not contained here.
|
||||
// DAO layer is not considered here
|
||||
type Registry struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type RegistryType `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Credential *Credential `json:"credential"`
|
||||
Insecure bool `json:"insecure"`
|
||||
Status string `json:"status"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type RegistryType `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Credential *Credential `json:"credential"`
|
||||
Insecure bool `json:"insecure"`
|
||||
Status string `json:"status"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
// RegistryQuery defines the query conditions for listing registries
|
||||
type RegistryQuery struct {
|
||||
// Name is name of the registry to query
|
||||
Name string
|
||||
models.Pagination
|
||||
// Pagination specifies the pagination
|
||||
Pagination *models.Pagination
|
||||
}
|
||||
|
66
src/replication/ng/registry/healthcheck.go
Normal file
66
src/replication/ng/registry/healthcheck.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// MinInterval defines the minimum interval to check registries' health status.
|
||||
const MinInterval = time.Second * 3
|
||||
|
||||
// HealthChecker is used to regularly check all registries' health status and update
|
||||
// check result to database
|
||||
type HealthChecker struct {
|
||||
interval time.Duration
|
||||
closing chan struct{}
|
||||
manager Manager
|
||||
}
|
||||
|
||||
// NewHealthChecker creates a new health checker
|
||||
// - interval specifies the time interval to perform health check for registries
|
||||
// - closing is a channel to stop the health checker
|
||||
func NewHealthChecker(interval time.Duration, closing chan struct{}) *HealthChecker {
|
||||
return &HealthChecker{
|
||||
interval: interval,
|
||||
manager: NewDefaultManager(),
|
||||
closing: closing,
|
||||
}
|
||||
}
|
||||
|
||||
// Run performs health check for all registries regularly
|
||||
func (c *HealthChecker) Run() {
|
||||
interval := c.interval
|
||||
if c.interval < MinInterval {
|
||||
interval = MinInterval
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
log.Infof("Start regular health check for registries with interval %v", interval)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := c.manager.HealthCheck(); err != nil {
|
||||
log.Errorf("Health check error: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Debug("Health Check succeeded")
|
||||
case <-c.closing:
|
||||
log.Info("Stop health checker")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
@ -15,10 +15,32 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry"
|
||||
"github.com/goharbor/harbor/src/common/utils/registry/auth"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao"
|
||||
"github.com/goharbor/harbor/src/replication/ng/dao/models"
|
||||
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||
)
|
||||
|
||||
// Manager manages registries
|
||||
// HealthStatus describes whether a target is healthy or not
|
||||
type HealthStatus string
|
||||
|
||||
const (
|
||||
// Healthy indicates target is healthy
|
||||
Healthy = "healthy"
|
||||
// Unhealthy indicates target is unhealthy
|
||||
Unhealthy = "unhealthy"
|
||||
// Unknown indicates health status of target is unknown
|
||||
Unknown = "unknown"
|
||||
)
|
||||
|
||||
// Manager defines the methods that a target manager should implement
|
||||
type Manager interface {
|
||||
// Add new registry
|
||||
Add(*model.Registry) (int64, error)
|
||||
@ -26,9 +48,273 @@ type Manager interface {
|
||||
List(...*model.RegistryQuery) (int64, []*model.Registry, error)
|
||||
// Get the specified registry
|
||||
Get(int64) (*model.Registry, error)
|
||||
// GetByName gets registry by name
|
||||
GetByName(name string) (*model.Registry, error)
|
||||
// Update the registry, the "props" are the properties of registry
|
||||
// that need to be updated
|
||||
Update(registry *model.Registry, props ...string) error
|
||||
// Remove the registry with the specified ID
|
||||
Remove(int64) error
|
||||
// HealthCheck checks health status of all registries and update result in database
|
||||
HealthCheck() error
|
||||
}
|
||||
|
||||
// DefaultManager implement the Manager interface
|
||||
type DefaultManager struct{}
|
||||
|
||||
// NewDefaultManager returns an instance of DefaultManger
|
||||
func NewDefaultManager() *DefaultManager {
|
||||
return &DefaultManager{}
|
||||
}
|
||||
|
||||
// Ensure *DefaultManager has implemented Manager interface.
|
||||
var _ Manager = (*DefaultManager)(nil)
|
||||
|
||||
// Get gets a registry by id
|
||||
func (m *DefaultManager) Get(id int64) (*model.Registry, error) {
|
||||
registry, err := dao.GetRegistry(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if registry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return fromDaoModel(registry)
|
||||
}
|
||||
|
||||
// GetByName gets a registry by its name
|
||||
func (m *DefaultManager) GetByName(name string) (*model.Registry, error) {
|
||||
registry, err := dao.GetRegistryByName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if registry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return fromDaoModel(registry)
|
||||
}
|
||||
|
||||
// List lists registries according to query provided.
|
||||
func (m *DefaultManager) List(query ...*model.RegistryQuery) (int64, []*model.Registry, error) {
|
||||
var registryQueries []*dao.ListRegistryQuery
|
||||
if len(query) > 0 {
|
||||
// limit being -1 indicates no pagination specified, result in all registries matching name returned.
|
||||
listQuery := &dao.ListRegistryQuery{
|
||||
Query: query[0].Name,
|
||||
Limit: -1,
|
||||
}
|
||||
if query[0].Pagination != nil {
|
||||
listQuery.Offset = query[0].Pagination.Page * query[0].Pagination.Size
|
||||
listQuery.Limit = query[0].Pagination.Size
|
||||
}
|
||||
|
||||
registryQueries = append(registryQueries, listQuery)
|
||||
}
|
||||
total, registries, err := dao.ListRegistries(registryQueries...)
|
||||
if err != nil {
|
||||
return -1, nil, err
|
||||
}
|
||||
|
||||
var results []*model.Registry
|
||||
for _, r := range registries {
|
||||
registry, err := fromDaoModel(r)
|
||||
if err != nil {
|
||||
return -1, nil, err
|
||||
}
|
||||
results = append(results, registry)
|
||||
}
|
||||
|
||||
return total, results, nil
|
||||
}
|
||||
|
||||
// Add adds a new registry
|
||||
func (m *DefaultManager) Add(registry *model.Registry) (int64, error) {
|
||||
r, err := toDaoModel(registry)
|
||||
if err != nil {
|
||||
log.Errorf("Convert registry model to dao layer model error: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
id, err := dao.AddRegistry(r)
|
||||
if err != nil {
|
||||
log.Errorf("Add registry error: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Update updates a registry
|
||||
func (m *DefaultManager) Update(registry *model.Registry, props ...string) error {
|
||||
// TODO(ChenDe): Only update the given props
|
||||
|
||||
r, err := toDaoModel(registry)
|
||||
if err != nil {
|
||||
log.Errorf("Convert registry model to dao layer model error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return dao.UpdateRegistry(r)
|
||||
}
|
||||
|
||||
// Remove deletes a registry
|
||||
func (m *DefaultManager) Remove(id int64) error {
|
||||
if err := dao.DeleteRegistry(id); err != nil {
|
||||
log.Errorf("Delete registry %d error: %v", id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealthCheck checks health status of every registries and update their status. It will check whether a registry
|
||||
// is reachable and the credential is valid
|
||||
func (m *DefaultManager) HealthCheck() error {
|
||||
_, registries, err := m.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errCount := 0
|
||||
for _, r := range registries {
|
||||
status, err := healthStatus(r)
|
||||
if err != nil {
|
||||
log.Warningf("Check health status for %s error: %v", r.URL, err)
|
||||
}
|
||||
r.Status = string(status)
|
||||
err = m.Update(r)
|
||||
if err != nil {
|
||||
log.Warningf("Update health status for '%s' error: %v", r.URL, err)
|
||||
errCount++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if errCount > 0 {
|
||||
return fmt.Errorf("%d out of %d registries failed to update health status", errCount, len(registries))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func healthStatus(r *model.Registry) (HealthStatus, error) {
|
||||
// TODO(ChenDe): Support other credential type like OAuth, for the moment, only basic auth is supported.
|
||||
if r.Credential.Type != model.CredentialTypeBasic {
|
||||
return Unknown, fmt.Errorf("unknown credential type '%s', only '%s' supported yet", r.Credential.Type, model.CredentialTypeBasic)
|
||||
}
|
||||
|
||||
// TODO(ChenDe): Support health check for other kinds of registry
|
||||
if r.Type != model.RegistryTypeHarbor {
|
||||
return Unknown, fmt.Errorf("unknown registry type '%s'", model.RegistryTypeHarbor)
|
||||
}
|
||||
|
||||
transport := registry.GetHTTPTransport(r.Insecure)
|
||||
credential := auth.NewBasicAuthCredential(r.Credential.AccessKey, r.Credential.AccessSecret)
|
||||
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
|
||||
Transport: transport,
|
||||
}, credential)
|
||||
registry, err := registry.NewRegistry(r.URL, &http.Client{
|
||||
Transport: registry.NewTransport(transport, authorizer),
|
||||
})
|
||||
if err != nil {
|
||||
return Unknown, err
|
||||
}
|
||||
|
||||
err = registry.Ping()
|
||||
if err != nil {
|
||||
return Unhealthy, err
|
||||
}
|
||||
|
||||
return Healthy, nil
|
||||
}
|
||||
|
||||
// decrypt checks whether access secret is set in the registry, if so, decrypt it.
|
||||
func decrypt(secret string) (string, error) {
|
||||
if len(secret) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
key, err := config.SecretKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
decrypted, err := utils.ReversibleDecrypt(secret, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
// encrypt checks whether access secret is set in the registry, if so, encrypt it.
|
||||
func encrypt(secret string) (string, error) {
|
||||
if len(secret) == 0 {
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
key, err := config.SecretKey()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
encrypted, err := utils.ReversibleEncrypt(secret, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// fromDaoModel converts DAO layer registry model to replication model.
|
||||
// Also, if access secret is provided, decrypt it.
|
||||
func fromDaoModel(registry *models.Registry) (*model.Registry, error) {
|
||||
decrypted, err := decrypt(registry.AccessSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := &model.Registry{
|
||||
ID: registry.ID,
|
||||
Name: registry.Name,
|
||||
Description: registry.Description,
|
||||
Type: model.RegistryType(registry.Type),
|
||||
URL: registry.URL,
|
||||
Credential: &model.Credential{
|
||||
Type: model.CredentialType(registry.CredentialType),
|
||||
AccessKey: registry.AccessKey,
|
||||
AccessSecret: decrypted,
|
||||
},
|
||||
Insecure: registry.Insecure,
|
||||
Status: registry.Health,
|
||||
CreationTime: registry.CreationTime,
|
||||
UpdateTime: registry.UpdateTime,
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// toDaoModel converts registry model from replication to DAO layer model.
|
||||
// Also, if access secret is provided, encrypt it.
|
||||
func toDaoModel(registry *model.Registry) (*models.Registry, error) {
|
||||
encrypted, err := encrypt(registry.Credential.AccessSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.Registry{
|
||||
ID: registry.ID,
|
||||
URL: registry.URL,
|
||||
Name: registry.Name,
|
||||
CredentialType: string(registry.Credential.Type),
|
||||
AccessKey: registry.Credential.AccessKey,
|
||||
AccessSecret: encrypted,
|
||||
Type: string(registry.Type),
|
||||
Insecure: registry.Insecure,
|
||||
Description: registry.Description,
|
||||
Health: registry.Status,
|
||||
CreationTime: registry.CreationTime,
|
||||
UpdateTime: registry.UpdateTime,
|
||||
}, nil
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package target
|
||||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
@ -38,7 +38,8 @@ var (
|
||||
|
||||
// Init the global variables
|
||||
func Init() error {
|
||||
// TODO init RegistryMgr
|
||||
// Init registry manager
|
||||
RegistryMgr = registry.NewDefaultManager()
|
||||
|
||||
// TODO init ExecutionMgr
|
||||
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/replication/models"
|
||||
"github.com/goharbor/harbor/src/replication/ng/model"
|
||||
)
|
||||
|
||||
// Replication holds information for a replication
|
||||
@ -32,7 +33,7 @@ type Replication struct {
|
||||
PolicyID int64
|
||||
OpUUID string
|
||||
Candidates []models.FilterItem
|
||||
Targets []*common_models.RepTarget
|
||||
Registries []*model.Registry
|
||||
Operation string
|
||||
}
|
||||
|
||||
@ -68,7 +69,7 @@ func (d *DefaultReplicator) Replicate(replication *Replication) error {
|
||||
operation = candidate.Operation
|
||||
}
|
||||
|
||||
for _, target := range replication.Targets {
|
||||
for _, registry := range replication.Registries {
|
||||
for repository, tags := range repositories {
|
||||
// create job in database
|
||||
id, err := dao.AddRepJob(common_models.RepJob{
|
||||
@ -84,7 +85,7 @@ func (d *DefaultReplicator) Replicate(replication *Replication) error {
|
||||
|
||||
// submit job to jobservice
|
||||
log.Debugf("submiting replication job to jobservice, repository: %s, tags: %v, operation: %s, target: %s",
|
||||
repository, tags, operation, target.URL)
|
||||
repository, tags, operation, registry.URL)
|
||||
job := &job_models.JobData{
|
||||
Metadata: &job_models.JobMetadata{
|
||||
JobKind: common_job.JobKindGeneric,
|
||||
@ -101,20 +102,20 @@ func (d *DefaultReplicator) Replicate(replication *Replication) error {
|
||||
"src_registry_url": config.InternalCoreURL(),
|
||||
"src_registry_insecure": false,
|
||||
"src_token_service_url": config.InternalTokenServiceEndpoint(),
|
||||
"dst_registry_url": target.URL,
|
||||
"dst_registry_insecure": target.Insecure,
|
||||
"dst_registry_username": target.Username,
|
||||
"dst_registry_password": target.Password,
|
||||
"dst_registry_url": registry.URL,
|
||||
"dst_registry_insecure": registry.Insecure,
|
||||
"dst_registry_username": registry.Credential.AccessKey,
|
||||
"dst_registry_password": registry.Credential.AccessSecret,
|
||||
}
|
||||
} else {
|
||||
job.Name = common_job.ImageDelete
|
||||
job.Parameters = map[string]interface{}{
|
||||
"repository": repository,
|
||||
"tags": tags,
|
||||
"dst_registry_url": target.URL,
|
||||
"dst_registry_insecure": target.Insecure,
|
||||
"dst_registry_username": target.Username,
|
||||
"dst_registry_password": target.Password,
|
||||
"dst_registry_url": registry.URL,
|
||||
"dst_registry_insecure": registry.Insecure,
|
||||
"dst_registry_username": registry.Credential.AccessKey,
|
||||
"dst_registry_password": registry.Credential.AccessSecret,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,63 +0,0 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package target
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
)
|
||||
|
||||
// Manager defines the methods that a target manager should implement
|
||||
type Manager interface {
|
||||
GetTarget(int64) (*models.RepTarget, error)
|
||||
}
|
||||
|
||||
// DefaultManager implement the Manager interface
|
||||
type DefaultManager struct{}
|
||||
|
||||
// NewDefaultManager returns an instance of DefaultManger
|
||||
func NewDefaultManager() *DefaultManager {
|
||||
return &DefaultManager{}
|
||||
}
|
||||
|
||||
// GetTarget ...
|
||||
func (d *DefaultManager) GetTarget(id int64) (*models.RepTarget, error) {
|
||||
target, err := dao.GetRepTarget(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if target == nil {
|
||||
return nil, fmt.Errorf("target '%d' does not exist", id)
|
||||
}
|
||||
|
||||
// decrypt the password
|
||||
if len(target.Password) > 0 {
|
||||
key, err := config.SecretKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pwd, err := utils.ReversibleDecrypt(target.Password, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target.Password = pwd
|
||||
}
|
||||
return target, nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user