mirror of
https://github.com/goharbor/harbor
synced 2025-04-08 04:49:39 +00:00
Merge pull request #3905 from pengpengshui/repEnhance
Add new replication rule module
This commit is contained in:
commit
a9877c3e0a
2
Makefile
2
Makefile
|
@ -306,6 +306,8 @@ modify_composefile_clair:
|
|||
@cp $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRTPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
@$(SEDCMD) -i 's/__postgresql_version__/$(CLAIRDBVERSION)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
@$(SEDCMD) -i 's/__clair_version__/$(CLAIRVERSION)-$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
@cp $(DOCKERCOMPOSEFILEPATH)/ha/$(DOCKERCOMPOSECLAIRTPLFILENAME) $(DOCKERCOMPOSEFILEPATH)/ha/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
@$(SEDCMD) -i 's/__clair_version__/$(CLAIRVERSION)-$(VERSIONTAG)/g' $(DOCKERCOMPOSEFILEPATH)/ha/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
|
||||
modify_sourcefiles:
|
||||
@echo "change mode of source files."
|
||||
|
|
|
@ -4,32 +4,31 @@ log:
|
|||
fields:
|
||||
service: registry
|
||||
storage:
|
||||
cache:
|
||||
layerinfo: inmemory
|
||||
filesystem:
|
||||
rootdirectory: /storage
|
||||
maintenance:
|
||||
uploadpurging:
|
||||
enabled: false
|
||||
delete:
|
||||
enabled: true
|
||||
cache:
|
||||
layerinfo: inmemory
|
||||
filesystem:
|
||||
rootdirectory: /storage
|
||||
maintenance:
|
||||
uploadpurging:
|
||||
enabled: false
|
||||
delete:
|
||||
enabled: true
|
||||
http:
|
||||
addr: :5000
|
||||
secret: placeholder
|
||||
debug:
|
||||
addr: localhost:5001
|
||||
addr: :5000
|
||||
secret: placeholder
|
||||
debug:
|
||||
addr: localhost:5001
|
||||
auth:
|
||||
token:
|
||||
issuer: harbor-token-issuer
|
||||
realm: $ui_url/service/token
|
||||
rootcertbundle: /etc/registry/root.crt
|
||||
service: harbor-registry
|
||||
|
||||
notifications:
|
||||
endpoints:
|
||||
- name: harbor
|
||||
disabled: false
|
||||
url: http://ui:8080/service/notifications
|
||||
timeout: 3000ms
|
||||
threshold: 5
|
||||
backoff: 1s
|
||||
- name: harbor
|
||||
disabled: false
|
||||
url: http://ui:8080/service/notifications
|
||||
timeout: 3000ms
|
||||
threshold: 5
|
||||
backoff: 1s
|
||||
|
|
|
@ -4,14 +4,14 @@ log:
|
|||
fields:
|
||||
service: registry
|
||||
storage:
|
||||
cache:
|
||||
layerinfo: redis
|
||||
Place_holder_for_Storage_configureation
|
||||
maintenance:
|
||||
uploadpurging:
|
||||
enabled: false
|
||||
delete:
|
||||
enabled: true
|
||||
cache:
|
||||
layerinfo: redis
|
||||
Place_holder_for_Storage_configureation
|
||||
maintenance:
|
||||
uploadpurging:
|
||||
enabled: false
|
||||
delete:
|
||||
enabled: true
|
||||
redis:
|
||||
addr: $redis_url
|
||||
db: 0
|
||||
|
@ -24,10 +24,10 @@ redis:
|
|||
idletimeout: 300s
|
||||
|
||||
http:
|
||||
addr: :5000
|
||||
secret: placeholder
|
||||
debug:
|
||||
addr: localhost:5001
|
||||
addr: :5000
|
||||
secret: placeholder
|
||||
debug:
|
||||
addr: localhost:5001
|
||||
auth:
|
||||
token:
|
||||
issuer: harbor-token-issuer
|
||||
|
@ -37,9 +37,9 @@ auth:
|
|||
|
||||
notifications:
|
||||
endpoints:
|
||||
- name: harbor
|
||||
disabled: false
|
||||
url: http://ui:8080/service/notifications
|
||||
timeout: 3000ms
|
||||
threshold: 5
|
||||
backoff: 1s
|
||||
- name: harbor
|
||||
disabled: false
|
||||
url: http://ui:8080/service/notifications
|
||||
timeout: 3000ms
|
||||
threshold: 5
|
||||
backoff: 1s
|
||||
|
|
|
@ -76,7 +76,7 @@ services:
|
|||
volumes:
|
||||
- ./common/config/ui/app.conf:/etc/ui/app.conf:z
|
||||
- ./common/config/ui/private_key.pem:/etc/ui/private_key.pem:z
|
||||
- ./common/config/ui/certificates/:/etc/ui/certifates/
|
||||
- ./common/config/ui/certificates/:/etc/ui/certificates/
|
||||
- /data/secretkey:/etc/ui/key:z
|
||||
- /data/ca_download/:/etc/ui/ca/:z
|
||||
- /data/psc/:/etc/ui/token/:z
|
||||
|
|
32
make/ha/docker-compose.clair.tpl
Normal file
32
make/ha/docker-compose.clair.tpl
Normal file
|
@ -0,0 +1,32 @@
|
|||
version: '2'
|
||||
services:
|
||||
ui:
|
||||
networks:
|
||||
harbor-clair:
|
||||
aliases:
|
||||
- harbor-ui
|
||||
jobservice:
|
||||
networks:
|
||||
- harbor-clair
|
||||
registry:
|
||||
networks:
|
||||
- harbor-clair
|
||||
clair:
|
||||
networks:
|
||||
- harbor-clair
|
||||
container_name: clair
|
||||
image: vmware/clair-photon:__clair_version__
|
||||
restart: always
|
||||
cpu_quota: 150000
|
||||
depends_on:
|
||||
- log
|
||||
volumes:
|
||||
- ./common/config/clair:/config
|
||||
logging:
|
||||
driver: "syslog"
|
||||
options:
|
||||
syslog-address: "tcp://127.0.0.1:1514"
|
||||
tag: "clair"
|
||||
networks:
|
||||
harbor-clair:
|
||||
external: false
|
|
@ -142,4 +142,5 @@ uaa_endpoint = uaa.mydomain.org
|
|||
uaa_clientid = id
|
||||
uaa_clientsecret = secret
|
||||
uaa_verify_cert = true
|
||||
uaa_ca_cert = /path/to/ca.pem
|
||||
#############
|
||||
|
|
|
@ -165,7 +165,7 @@ if [ $with_notary ] && [ ! $harbor_ha ]
|
|||
then
|
||||
prepare_para="${prepare_para} --with-notary"
|
||||
fi
|
||||
if [ $with_clair ] && [ ! $harbor_ha ]
|
||||
if [ $with_clair ]
|
||||
then
|
||||
prepare_para="${prepare_para} --with-clair"
|
||||
fi
|
||||
|
@ -182,7 +182,7 @@ if [ $with_notary ] && [ ! $harbor_ha ]
|
|||
then
|
||||
docker_compose_list="${docker_compose_list} -f docker-compose.notary.yml"
|
||||
fi
|
||||
if [ $with_clair ] && [ ! $harbor_ha ]
|
||||
if [ $with_clair ]
|
||||
then
|
||||
docker_compose_list="${docker_compose_list} -f docker-compose.clair.yml"
|
||||
fi
|
||||
|
@ -199,6 +199,8 @@ if [ $harbor_ha ]
|
|||
then
|
||||
mv docker-compose.yml docker-compose.yml.bak
|
||||
cp ha/docker-compose.yml docker-compose.yml
|
||||
mv docker-compose.clair.yml docker-compose.clair.yml.bak
|
||||
cp ha/docker-compose.clair.yml docker-compose.clair.yml
|
||||
fi
|
||||
docker-compose $docker_compose_list up -d
|
||||
|
||||
|
|
21
make/prepare
21
make/prepare
|
@ -30,8 +30,13 @@ def validate(conf, args):
|
|||
redis_url = rcp.get("configuration", "redis_url")
|
||||
if redis_url is None or len(redis_url) < 1:
|
||||
raise Exception("Error: In HA mode redis is required redis_url need to point to an redis cluster")
|
||||
if args.notary_mode or args.clair_mode:
|
||||
raise Exception("Error: HA mode doesn't support clair and notary currently")
|
||||
if args.notary_mode:
|
||||
raise Exception("Error: HA mode doesn't support Notary currently")
|
||||
if args.clair_mode:
|
||||
clair_db_host = rcp.get("configuration", "clair_db_host")
|
||||
if "postgres" == clair_db_host:
|
||||
raise Exception("Error: In HA mode, clair_db_host in harbor.cfg needs to point to an external Postgres DB address.")
|
||||
|
||||
cert_path = rcp.get("configuration", "ssl_cert")
|
||||
cert_key_path = rcp.get("configuration", "ssl_cert_key")
|
||||
shared_cert_key = os.path.join(base_dir, "ha", os.path.basename(cert_key_path))
|
||||
|
@ -245,6 +250,7 @@ uaa_endpoint = rcp.get("configuration", "uaa_endpoint")
|
|||
uaa_clientid = rcp.get("configuration", "uaa_clientid")
|
||||
uaa_clientsecret = rcp.get("configuration", "uaa_clientsecret")
|
||||
uaa_verify_cert = rcp.get("configuration", "uaa_verify_cert")
|
||||
uaa_ca_cert = rcp.get("configuration", "uaa_ca_cert")
|
||||
|
||||
secret_key = get_secret_key(secretkey_path)
|
||||
log_rotate_count = rcp.get("configuration", "log_rotate_count")
|
||||
|
@ -275,6 +281,7 @@ log_config_dir = prep_conf_dir (config_dir, "log")
|
|||
adminserver_conf_env = os.path.join(config_dir, "adminserver", "env")
|
||||
ui_conf_env = os.path.join(config_dir, "ui", "env")
|
||||
ui_conf = os.path.join(config_dir, "ui", "app.conf")
|
||||
ui_cert_dir = os.path.join(config_dir, "ui", "certificates")
|
||||
jobservice_conf = os.path.join(config_dir, "jobservice", "app.conf")
|
||||
registry_conf = os.path.join(config_dir, "registry", "config.yml")
|
||||
db_conf_env = os.path.join(config_dir, "db", "env")
|
||||
|
@ -382,6 +389,16 @@ shutil.copyfile(os.path.join(templates_dir, "jobservice", "app.conf"), jobservic
|
|||
print("Generated configuration file: %s" % ui_conf)
|
||||
shutil.copyfile(os.path.join(templates_dir, "ui", "app.conf"), ui_conf)
|
||||
|
||||
if auth_mode == "uaa_auth":
|
||||
if os.path.isfile(uaa_ca_cert):
|
||||
if not os.path.isdir(ui_cert_dir):
|
||||
os.makedirs(ui_cert_dir, mode=0o600)
|
||||
ui_uaa_ca = os.path.join(ui_cert_dir, "uaa_ca.pem")
|
||||
print("Copying UAA CA cert to %s" % ui_uaa_ca)
|
||||
shutil.copyfile(uaa_ca_cert, ui_uaa_ca)
|
||||
else:
|
||||
print("Can not find UAA CA cert: %s, skip" % uaa_ca_cert)
|
||||
|
||||
|
||||
def validate_crt_subj(dirty_subj):
|
||||
subj_list = [item for item in dirty_subj.strip().split("/") \
|
||||
|
|
|
@ -46,7 +46,7 @@ func TestAuthModeCanBeModified(t *testing.T) {
|
|||
t.Fatalf("failed to register user: %v", err)
|
||||
}
|
||||
defer func(id int64) {
|
||||
if err := deleteUser(id); err != nil {
|
||||
if err := CleanUser(id); err != nil {
|
||||
t.Fatalf("failed to delete user %d: %v", id, err)
|
||||
}
|
||||
}(id)
|
||||
|
@ -68,4 +68,4 @@ func TestAuthModeCanBeModified(t *testing.T) {
|
|||
t.Errorf("unexpected result: %t != %t", flag, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,9 +258,12 @@ func DeleteUser(userID int) error {
|
|||
}
|
||||
|
||||
// ChangeUserProfile ...
|
||||
func ChangeUserProfile(user models.User) error {
|
||||
func ChangeUserProfile(user models.User, cols ...string) error {
|
||||
o := GetOrmer()
|
||||
if _, err := o.Update(&user, "Email", "Realname", "Comment"); err != nil {
|
||||
if len(cols) == 0 {
|
||||
cols = []string{"Email", "Realname", "Comment"}
|
||||
}
|
||||
if _, err := o.Update(&user, cols...); err != nil {
|
||||
log.Errorf("update user failed, error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
@ -290,3 +293,12 @@ func OnBoardUser(u *models.User) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//CleanUser - Clean this user information from DB
|
||||
func CleanUser(id int64) error {
|
||||
if _, err := GetOrmer().QueryTable(&models.User{}).
|
||||
Filter("UserID", id).Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestDeleteUser(t *testing.T) {
|
|||
t.Fatalf("failed to register user: %v", err)
|
||||
}
|
||||
defer func(id int64) {
|
||||
if err := deleteUser(id); err != nil {
|
||||
if err := CleanUser(id); err != nil {
|
||||
t.Fatalf("failed to delete user %d: %v", id, err)
|
||||
}
|
||||
}(id)
|
||||
|
@ -88,13 +88,5 @@ func TestOnBoardUser(t *testing.T) {
|
|||
err = OnBoardUser(u)
|
||||
assert.Nil(err)
|
||||
assert.True(u.UserID == id)
|
||||
deleteUser(int64(id))
|
||||
}
|
||||
|
||||
func deleteUser(id int64) error {
|
||||
if _, err := GetOrmer().QueryTable(&models.User{}).
|
||||
Filter("UserID", id).Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
CleanUser(int64(id))
|
||||
}
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
}
|
|
@ -89,14 +89,15 @@ func (r *RepTarget) Valid(v *validation.Validation) {
|
|||
v.SetError("name", "max length is 64")
|
||||
}
|
||||
|
||||
if len(r.URL) == 0 {
|
||||
v.SetError("endpoint", "can not be empty")
|
||||
}
|
||||
|
||||
r.URL = utils.FormatEndpoint(r.URL)
|
||||
|
||||
if len(r.URL) > 64 {
|
||||
v.SetError("endpoint", "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
|
||||
|
|
131
src/common/models/target_test.go
Normal file
131
src/common/models/target_test.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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 models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/astaxie/beego/validation"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidOfTarget(t *testing.T) {
|
||||
cases := []struct {
|
||||
target RepTarget
|
||||
err bool
|
||||
expected RepTarget
|
||||
}{
|
||||
// name is null
|
||||
{
|
||||
RepTarget{
|
||||
Name: "",
|
||||
},
|
||||
true,
|
||||
RepTarget{}},
|
||||
|
||||
// url is null
|
||||
{
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "",
|
||||
},
|
||||
true,
|
||||
RepTarget{},
|
||||
},
|
||||
|
||||
// invalid url
|
||||
{
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "ftp://example.com",
|
||||
},
|
||||
true,
|
||||
RepTarget{},
|
||||
},
|
||||
|
||||
// invalid url
|
||||
{
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "ftp://example.com",
|
||||
},
|
||||
true,
|
||||
RepTarget{},
|
||||
},
|
||||
|
||||
// valid url
|
||||
{
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "example.com",
|
||||
},
|
||||
false,
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com",
|
||||
},
|
||||
},
|
||||
|
||||
// valid url
|
||||
{
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com",
|
||||
},
|
||||
false,
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com",
|
||||
},
|
||||
},
|
||||
|
||||
// valid url
|
||||
{
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "https://example.com",
|
||||
},
|
||||
false,
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "https://example.com",
|
||||
},
|
||||
},
|
||||
|
||||
// valid url
|
||||
{
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com/redirect?key=value",
|
||||
},
|
||||
false,
|
||||
RepTarget{
|
||||
Name: "endpoint01",
|
||||
URL: "http://example.com/redirect",
|
||||
}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
v := &validation.Validation{}
|
||||
c.target.Valid(v)
|
||||
if c.err {
|
||||
require.True(t, v.HasErrors())
|
||||
continue
|
||||
}
|
||||
require.False(t, v.HasErrors())
|
||||
assert.Equal(t, c.expected, c.target)
|
||||
}
|
||||
}
|
|
@ -129,7 +129,7 @@ func (r *Registry) Catalog() ([]string, error) {
|
|||
|
||||
// Ping ...
|
||||
func (r *Registry) Ping() error {
|
||||
req, err := http.NewRequest("GET", buildPingURL(r.Endpoint.String()), nil)
|
||||
req, err := http.NewRequest(http.MethodHead, buildPingURL(r.Endpoint.String()), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import (
|
|||
func TestPing(t *testing.T) {
|
||||
server := test.NewServer(
|
||||
&test.RequestHandlerMapping{
|
||||
Method: "GET",
|
||||
Method: http.MethodHead,
|
||||
Pattern: "/v2/",
|
||||
Handler: test.Handler(nil),
|
||||
})
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
|
@ -179,16 +180,20 @@ func NewDefaultClient(cfg *ClientConfig) (Client, error) {
|
|||
InsecureSkipVerify: cfg.SkipTLSVerify,
|
||||
}
|
||||
if !cfg.SkipTLSVerify && len(cfg.CARootPath) > 0 {
|
||||
content, err := ioutil.ReadFile(cfg.CARootPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
//Do not throw error if the certificate is malformed, so we can put a place holder.
|
||||
if ok := pool.AppendCertsFromPEM(content); !ok {
|
||||
log.Warningf("Failed to append certificate to cert pool, cert path: %s", cfg.CARootPath)
|
||||
if _, err := os.Stat(cfg.CARootPath); !os.IsNotExist(err) {
|
||||
content, err := ioutil.ReadFile(cfg.CARootPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
//Do not throw error if the certificate is malformed, so we can put a place holder.
|
||||
if ok := pool.AppendCertsFromPEM(content); !ok {
|
||||
log.Warningf("Failed to append certificate to cert pool, cert path: %s", cfg.CARootPath)
|
||||
} else {
|
||||
tc.RootCAs = pool
|
||||
}
|
||||
} else {
|
||||
tc.RootCAs = pool
|
||||
log.Warningf("The root certificate file %s is not found, skip configuring root cert in UAA client.", cfg.CARootPath)
|
||||
}
|
||||
}
|
||||
hc := &http.Client{
|
||||
|
|
|
@ -98,7 +98,7 @@ func TestNewClientWithCACert(t *testing.T) {
|
|||
CARootPath: "/notexist",
|
||||
}
|
||||
_, err := NewDefaultClient(cfg)
|
||||
assert.NotNil(err)
|
||||
assert.Nil(err)
|
||||
//Skip if it's malformed.
|
||||
cfg.CARootPath = path.Join(currPath(), "test", "non-ca.pem")
|
||||
_, err = NewDefaultClient(cfg)
|
||||
|
|
|
@ -29,27 +29,24 @@ import (
|
|||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// FormatEndpoint formats endpoint
|
||||
func FormatEndpoint(endpoint string) string {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
// ParseEndpoint parses endpoint to a URL
|
||||
func ParseEndpoint(endpoint string) (*url.URL, error) {
|
||||
endpoint = strings.Trim(endpoint, " ")
|
||||
endpoint = strings.TrimRight(endpoint, "/")
|
||||
if !strings.HasPrefix(endpoint, "http://") &&
|
||||
!strings.HasPrefix(endpoint, "https://") {
|
||||
if len(endpoint) == 0 {
|
||||
return nil, fmt.Errorf("empty URL")
|
||||
}
|
||||
i := strings.Index(endpoint, "://")
|
||||
if i >= 0 {
|
||||
scheme := endpoint[:i]
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return nil, fmt.Errorf("invalid scheme: %s", scheme)
|
||||
}
|
||||
} else {
|
||||
endpoint = "http://" + endpoint
|
||||
}
|
||||
|
||||
return endpoint
|
||||
}
|
||||
|
||||
// ParseEndpoint parses endpoint to a URL
|
||||
func ParseEndpoint(endpoint string) (*url.URL, error) {
|
||||
endpoint = FormatEndpoint(endpoint)
|
||||
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
return url.ParseRequestURI(endpoint)
|
||||
}
|
||||
|
||||
// ParseRepository splits a repository into two parts: project and rest
|
||||
|
|
|
@ -23,37 +23,30 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseEndpoint(t *testing.T) {
|
||||
endpoint := "example.com"
|
||||
u, err := ParseEndpoint(endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse endpoint %s: %v", endpoint, err)
|
||||
cases := []struct {
|
||||
input string
|
||||
err bool
|
||||
expected string
|
||||
}{
|
||||
{" example.com/ ", false, "http://example.com"},
|
||||
{"ftp://example.com", true, ""},
|
||||
{"http://example.com", false, "http://example.com"},
|
||||
{"https://example.com", false, "https://example.com"},
|
||||
{"http://example!@#!?//#", true, ""},
|
||||
}
|
||||
|
||||
if u.String() != "http://example.com" {
|
||||
t.Errorf("unexpected endpoint: %s != %s", endpoint, "http://example.com")
|
||||
}
|
||||
|
||||
endpoint = "https://example.com"
|
||||
u, err = ParseEndpoint(endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse endpoint %s: %v", endpoint, err)
|
||||
}
|
||||
|
||||
if u.String() != "https://example.com" {
|
||||
t.Errorf("unexpected endpoint: %s != %s", endpoint, "https://example.com")
|
||||
}
|
||||
|
||||
endpoint = " example.com/ "
|
||||
u, err = ParseEndpoint(endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse endpoint %s: %v", endpoint, err)
|
||||
}
|
||||
|
||||
if u.String() != "http://example.com" {
|
||||
t.Errorf("unexpected endpoint: %s != %s", endpoint, "http://example.com")
|
||||
for _, c := range cases {
|
||||
u, err := ParseEndpoint(c.input)
|
||||
if c.err {
|
||||
require.NotNil(t, err)
|
||||
continue
|
||||
}
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, c.expected, u.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -106,7 +106,9 @@ func (e *EmailAPI) Ping() {
|
|||
addr := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
if err := email.Ping(addr, identity, username,
|
||||
password, pingEmailTimeout, ssl, insecure); err != nil {
|
||||
log.Debugf("ping %s failed: %v", addr, err)
|
||||
e.CustomAbort(http.StatusBadRequest, err.Error())
|
||||
log.Errorf("failed to ping email server: %v", err)
|
||||
// do not return any detail information of the error, or may cause SSRF security issue #3755
|
||||
e.RenderError(http.StatusBadRequest, "failed to ping email server")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ package api
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -36,7 +35,7 @@ func TestPingEmail(t *testing.T) {
|
|||
|
||||
assert.Equal(401, code, "the status code of ping email server with non-admin user should be 401")
|
||||
|
||||
//case 2: bad request
|
||||
//case 2: empty email host
|
||||
settings := `{
|
||||
"email_host": ""
|
||||
}`
|
||||
|
@ -47,7 +46,7 @@ func TestPingEmail(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
assert.Equal(400, code, "the status code of ping email server should be 400")
|
||||
assert.Equal(400, code)
|
||||
|
||||
//case 3: secure connection with admin role
|
||||
settings = `{
|
||||
|
@ -58,18 +57,13 @@ func TestPingEmail(t *testing.T) {
|
|||
"email_ssl": true
|
||||
}`
|
||||
|
||||
code, body, err := apiTest.PingEmail(*admin, []byte(settings))
|
||||
code, _, err = apiTest.PingEmail(*admin, []byte(settings))
|
||||
if err != nil {
|
||||
t.Errorf("failed to test ping email server: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(400, code, "the status code of ping email server should be 400")
|
||||
|
||||
if !strings.Contains(body, "535") {
|
||||
t.Errorf("unexpected error: %s does not contains 535", body)
|
||||
return
|
||||
}
|
||||
assert.Equal(400, code)
|
||||
|
||||
//case 4: ping email server whose settings are read from config
|
||||
code, _, err = apiTest.PingEmail(*admin, nil)
|
||||
|
@ -78,5 +72,5 @@ func TestPingEmail(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
assert.Equal(400, code, "the status code of ping email server should be 400")
|
||||
assert.Equal(400, code)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ type LdapAPI struct {
|
|||
}
|
||||
|
||||
const (
|
||||
pingErrorMessage = "LDAP connection test failed!"
|
||||
pingErrorMessage = "LDAP connection test failed"
|
||||
loadSystemErrorMessage = "Can't load system configuration!"
|
||||
canNotOpenLdapSession = "Can't open LDAP session!"
|
||||
searchLdapFailMessage = "LDAP search failed!"
|
||||
|
@ -72,6 +72,7 @@ func (l *LdapAPI) Ping() {
|
|||
|
||||
if err != nil {
|
||||
log.Errorf("ldap connect fail, error: %v", err)
|
||||
// do not return any detail information of the error, or may cause SSRF security issue #3755
|
||||
l.RenderError(http.StatusBadRequest, pingErrorMessage)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -16,15 +16,12 @@ package api
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/common/utils/registry"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
|
@ -60,27 +57,15 @@ func (t *TargetAPI) Prepare() {
|
|||
|
||||
func (t *TargetAPI) ping(endpoint, username, password string, insecure bool) {
|
||||
registry, err := newRegistryClient(endpoint, insecure, username, password)
|
||||
if err != nil {
|
||||
// timeout, dns resolve error, connection refused, etc.
|
||||
if urlErr, ok := err.(*url.Error); ok {
|
||||
if netErr, ok := urlErr.Err.(net.Error); ok {
|
||||
t.CustomAbort(http.StatusBadRequest, netErr.Error())
|
||||
}
|
||||
|
||||
t.CustomAbort(http.StatusBadRequest, urlErr.Error())
|
||||
}
|
||||
|
||||
log.Errorf("failed to create registry client: %#v", err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
if err == nil {
|
||||
err = registry.Ping()
|
||||
}
|
||||
|
||||
if err = registry.Ping(); err != nil {
|
||||
if regErr, ok := err.(*registry_error.HTTPError); ok {
|
||||
t.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
log.Errorf("failed to ping registry %s: %v", registry.Endpoint.String(), err)
|
||||
t.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +102,14 @@ func (t *TargetAPI) Ping() {
|
|||
}
|
||||
|
||||
if req.Endpoint != nil {
|
||||
target.URL = *req.Endpoint
|
||||
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
|
||||
|
@ -129,11 +121,6 @@ func (t *TargetAPI) Ping() {
|
|||
target.Insecure = *req.Insecure
|
||||
}
|
||||
|
||||
if len(target.URL) == 0 {
|
||||
t.HandleBadRequest("empty endpoint")
|
||||
return
|
||||
}
|
||||
|
||||
t.ping(target.URL, target.Username, target.Password, target.Insecure)
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,34 @@ type AuthenticateHelper interface {
|
|||
OnBoardUser(u *models.User) error
|
||||
// Get user information from account repository
|
||||
SearchUser(username string) (*models.User, error)
|
||||
// Update user information after authenticate, such as Onboard or sync info etc
|
||||
PostAuthenticate(u *models.User) error
|
||||
}
|
||||
|
||||
// DefaultAuthenticateHelper - default AuthenticateHelper implementation
|
||||
type DefaultAuthenticateHelper struct {
|
||||
}
|
||||
|
||||
// Authenticate ...
|
||||
func (d *DefaultAuthenticateHelper) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// OnBoardUser will check if a user exists in user table, if not insert the user and
|
||||
// put the id in the pointer of user model, if it does exist, fill in the user model based
|
||||
// on the data record of the user
|
||||
func (d *DefaultAuthenticateHelper) OnBoardUser(u *models.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//SearchUser - Get user information from account repository
|
||||
func (d *DefaultAuthenticateHelper) SearchUser(username string) (*models.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
//PostAuthenticate - Update user information after authenticate, such as Onboard or sync info etc
|
||||
func (d *DefaultAuthenticateHelper) PostAuthenticate(u *models.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var registry = make(map[string]AuthenticateHelper)
|
||||
|
@ -79,6 +107,9 @@ func Login(m models.AuthModel) (*models.User, error) {
|
|||
lock.Lock(m.Principal)
|
||||
time.Sleep(frozenTime)
|
||||
}
|
||||
|
||||
authenticator.PostAuthenticate(user)
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
|
@ -112,3 +143,12 @@ func SearchUser(username string) (*models.User, error) {
|
|||
}
|
||||
return helper.SearchUser(username)
|
||||
}
|
||||
|
||||
// PostAuthenticate -
|
||||
func PostAuthenticate(u *models.User) error {
|
||||
helper, err := getHelper()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return helper.PostAuthenticate(u)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ import (
|
|||
)
|
||||
|
||||
// Auth implements Authenticator interface to authenticate user against DB.
|
||||
type Auth struct{}
|
||||
type Auth struct {
|
||||
auth.DefaultAuthenticateHelper
|
||||
}
|
||||
|
||||
// Authenticate calls dao to authenticate user.
|
||||
func (d *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
||||
|
@ -32,12 +34,6 @@ func (d *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
|||
return u, nil
|
||||
}
|
||||
|
||||
// OnBoardUser - Dummy implementation when auth_mod is db_auth
|
||||
func (d *Auth) OnBoardUser(user *models.User) error {
|
||||
//No need to create user in local database
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchUser - Check if user exist in local db
|
||||
func (d *Auth) SearchUser(username string) (*models.User, error) {
|
||||
var queryCondition = models.User{
|
||||
|
|
|
@ -16,6 +16,7 @@ package ldap
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
|
@ -26,7 +27,9 @@ import (
|
|||
)
|
||||
|
||||
// Auth implements AuthenticateHelper interface to authenticate against LDAP
|
||||
type Auth struct{}
|
||||
type Auth struct {
|
||||
auth.DefaultAuthenticateHelper
|
||||
}
|
||||
|
||||
// Authenticate checks user's credential against LDAP based on basedn template and LDAP URL,
|
||||
// if the check is successful a dummy record will be inserted into DB, such that this user can
|
||||
|
@ -68,7 +71,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
|||
|
||||
u := models.User{}
|
||||
u.Username = ldapUsers[0].Username
|
||||
u.Email = ldapUsers[0].Email
|
||||
u.Email = strings.TrimSpace(ldapUsers[0].Email)
|
||||
u.Realname = ldapUsers[0].Realname
|
||||
|
||||
dn := ldapUsers[0].DN
|
||||
|
@ -78,34 +81,7 @@ func (l *Auth) Authenticate(m models.AuthModel) (*models.User, error) {
|
|||
log.Warningf("Failed to bind user, username: %s, dn: %s, error: %v", u.Username, dn, err)
|
||||
return nil, nil
|
||||
}
|
||||
exist, err := dao.UserExists(u, "username")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if exist {
|
||||
currentUser, err := dao.GetUser(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.UserID = currentUser.UserID
|
||||
u.HasAdminRole = currentUser.HasAdminRole
|
||||
} else {
|
||||
var user models.User
|
||||
user.Username = ldapUsers[0].Username
|
||||
user.Email = ldapUsers[0].Email
|
||||
user.Realname = ldapUsers[0].Realname
|
||||
|
||||
err = auth.OnBoardUser(&user)
|
||||
if err != nil || user.UserID <= 0 {
|
||||
log.Errorf("Can't import user %s, error: %v", ldapUsers[0].Username, err)
|
||||
return nil, fmt.Errorf("can't import user %s, error: %v", ldapUsers[0].Username, err)
|
||||
}
|
||||
u.UserID = user.UserID
|
||||
}
|
||||
|
||||
return &u, nil
|
||||
|
||||
}
|
||||
|
||||
// OnBoardUser will check if a user exists in user table, if not insert the user and
|
||||
|
@ -153,6 +129,52 @@ func (l *Auth) SearchUser(username string) (*models.User, error) {
|
|||
return &user, nil
|
||||
}
|
||||
|
||||
//PostAuthenticate -- If user exist in harbor DB, sync email address, if not exist, call OnBoardUser
|
||||
func (l *Auth) PostAuthenticate(u *models.User) error {
|
||||
|
||||
exist, err := dao.UserExists(*u, "username")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exist {
|
||||
queryCondition := models.User{
|
||||
Username: u.Username,
|
||||
}
|
||||
dbUser, err := dao.GetUser(queryCondition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dbUser == nil {
|
||||
fmt.Printf("User not found in DB %+v", u)
|
||||
return nil
|
||||
}
|
||||
u.UserID = dbUser.UserID
|
||||
u.HasAdminRole = dbUser.HasAdminRole
|
||||
|
||||
if dbUser.Email != u.Email {
|
||||
Re := regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
|
||||
if !Re.MatchString(u.Email) {
|
||||
log.Debugf("Not a valid email address: %v, skip to sync", u.Email)
|
||||
} else {
|
||||
dao.ChangeUserProfile(*u, "Email")
|
||||
}
|
||||
u.Email = dbUser.Email
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
err = auth.OnBoardUser(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.UserID <= 0 {
|
||||
return fmt.Errorf("Can not OnBoardUser %v", u)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
auth.Register("ldap_auth", &Auth{})
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
|
@ -204,3 +205,59 @@ func TestAuthenticateHelperSearchUser(t *testing.T) {
|
|||
t.Error("Failed to search user test")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostAuthentication(t *testing.T) {
|
||||
|
||||
assert := assert.New(t)
|
||||
user1 := &models.User{
|
||||
Username: "test003",
|
||||
Email: "test003@vmware.com",
|
||||
Realname: "test003",
|
||||
}
|
||||
|
||||
queryCondition := models.User{
|
||||
Username: "test003",
|
||||
Realname: "test003",
|
||||
}
|
||||
|
||||
err := auth.OnBoardUser(user1)
|
||||
assert.Nil(err)
|
||||
|
||||
user2 := &models.User{
|
||||
Username: "test003",
|
||||
Email: "234invalidmail@@@@@",
|
||||
}
|
||||
|
||||
auth.PostAuthenticate(user2)
|
||||
|
||||
dbUser, err := dao.GetUser(queryCondition)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user, error %v", err)
|
||||
}
|
||||
assert.EqualValues("test003@vmware.com", dbUser.Email)
|
||||
|
||||
user3 := &models.User{
|
||||
Username: "test003",
|
||||
}
|
||||
|
||||
auth.PostAuthenticate(user3)
|
||||
dbUser, err = dao.GetUser(queryCondition)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user, error %v", err)
|
||||
}
|
||||
assert.EqualValues("test003@vmware.com", dbUser.Email)
|
||||
|
||||
user4 := &models.User{
|
||||
Username: "test003",
|
||||
Email: "test003@example.com",
|
||||
}
|
||||
|
||||
auth.PostAuthenticate(user4)
|
||||
|
||||
dbUser, err = dao.GetUser(queryCondition)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get user, error %v", err)
|
||||
}
|
||||
assert.EqualValues("test003@example.com", dbUser.Email)
|
||||
dao.CleanUser(int64(dbUser.UserID))
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ package uaa
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
@ -38,6 +39,7 @@ func CreateClient() (uaa.Client, error) {
|
|||
ClientSecret: UAASettings.ClientSecret,
|
||||
Endpoint: UAASettings.Endpoint,
|
||||
SkipTLSVerify: !UAASettings.VerifyCert,
|
||||
CARootPath: os.Getenv("UAA_CA_ROOT"),
|
||||
}
|
||||
return uaa.NewDefaultClient(cfg)
|
||||
}
|
||||
|
@ -46,6 +48,7 @@ func CreateClient() (uaa.Client, error) {
|
|||
type Auth struct {
|
||||
sync.Mutex
|
||||
client uaa.Client
|
||||
auth.DefaultAuthenticateHelper
|
||||
}
|
||||
|
||||
//Authenticate ...
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "harbor-ui",
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.6",
|
||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||
"scripts": {
|
||||
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "harbor-ui",
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.6",
|
||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||
"author": "VMware",
|
||||
"module": "index.js",
|
||||
|
|
|
@ -23,7 +23,6 @@ export const CONFIRMATION_DIALOG_TEMPLATE: string = `
|
|||
<ng-template [ngSwitchCase]="0">
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="confirm()">{{'BUTTON.CONFIRM' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="cancel()">{{'BUTTON.CLOSE' | translate}}</button>
|
||||
</ng-template>
|
||||
<ng-template [ngSwitchCase]="1">
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.NO' | translate}}</button>
|
||||
|
@ -31,7 +30,7 @@ export const CONFIRMATION_DIALOG_TEMPLATE: string = `
|
|||
</ng-template>
|
||||
<ng-template [ngSwitchCase]="2">
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="isDelete">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-danger" (click)="confirm()" [hidden]="isDelete">{{'BUTTON.DELETE' | translate}}</button>
|
||||
<button type="button" class="btn btn-danger" (click)="delete()" [hidden]="isDelete">{{'BUTTON.DELETE' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="cancel()" [disabled]="!batchOverStatus" [hidden]="!isDelete">{{'BUTTON.CLOSE' | translate}}</button>
|
||||
</ng-template>
|
||||
<ng-template [ngSwitchCase]="3">
|
||||
|
|
|
@ -98,8 +98,8 @@ export class ConfirmationDialogComponent {
|
|||
this.close();
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
if(!this.message){//Inproper condition
|
||||
delete(): void {
|
||||
if (!this.message){//Inproper condition
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
@ -118,4 +118,21 @@ export class ConfirmationDialogComponent {
|
|||
);
|
||||
this.confirmAction.emit(message);
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
if (!this.message){//Inproper condition
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let data: any = this.message.data ? this.message.data : {};
|
||||
let target = this.message.targetId ? this.message.targetId : ConfirmationTargets.EMPTY;
|
||||
let message = new ConfirmationAcknowledgement(
|
||||
ConfirmationState.CONFIRMED,
|
||||
data,
|
||||
target
|
||||
);
|
||||
this.confirmAction.emit(message);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -214,7 +214,7 @@ export class HarborLibraryModule {
|
|||
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
|
||||
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
|
||||
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
|
||||
//Do initializing
|
||||
// Do initializing
|
||||
TranslateServiceInitializer,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
|
|
|
@ -1,40 +1,28 @@
|
|||
export const LIST_REPLICATION_RULE_TEMPLATE: string = `
|
||||
<div style="margin-top: -24px;">
|
||||
<div>
|
||||
<clr-datagrid [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedRow" (clrDgSingleSelectedChange)="selectedChange()">
|
||||
<clr-dg-action-bar>
|
||||
<div class="btn-group">
|
||||
<button type="button" *ngIf="creationAvailable" class="btn btn-sm btn-secondary" (click)="openModal()">{{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button>
|
||||
<button type="button" *ngIf="!creationAvailable" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="editRule(selectedRow)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
|
||||
<button type="button" *ngIf="!creationAvailable" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
|
||||
<div class="btn-group" *ngIf="opereateAvailable">
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="openModal()">{{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="editRule(selectedRow)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="replicateRule(selectedRow)">{{'REPLICATION.REPLICATE' | translate}}</button>
|
||||
</div>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'project_name'" *ngIf="!projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'projects'" *ngIf="!projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'description'">{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'target_name'">{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="enabledComparator">{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'targets'">{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'trigger'">{{'REPLICATION.SCHEDULE' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'REPLICATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let p of changedRules" [clrDgItem]="p" (click)="selectRule(p)" [style.backgroundColor]="(projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''">
|
||||
|
||||
<clr-dg-cell>
|
||||
<ng-template [ngIf]="!projectScope">
|
||||
<a href="javascript:void(0)" (click)="redirectTo(p)">{{p.name}}</a>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="projectScope">
|
||||
{{p.name}}
|
||||
</ng-template>
|
||||
<clr-dg-cell>{{p.name}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="!projectScope">
|
||||
<a href="javascript:void(0)" (click)="redirectTo(p)">{{p.projects?.length>0 ? p.projects[0].name : ''}}</a>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="!projectScope">{{p.project_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{p.description ? p.description : '-'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{p.target_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<ng-template [ngIf]="p.start_time === nullTime">-</ng-template>
|
||||
<ng-template [ngIf]="p.start_time !== nullTime">{{p.start_time | date: 'short'}}</ng-template>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
{{ (p.enabled === 1 ? 'REPLICATION.ENABLED' : 'REPLICATION.DISABLED') | translate}}
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{p.targets?.length>0 ? p.targets[0].name : ''}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{p.trigger ? p.trigger.kind : ''}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'REPLICATION.OF' | translate}} </span>{{pagination.totalItems }} {{'REPLICATION.ITEMS' | translate}}
|
||||
|
|
|
@ -67,6 +67,7 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
|||
@Output() toggleOne = new EventEmitter<ReplicationRule>();
|
||||
@Output() redirect = new EventEmitter<ReplicationRule>();
|
||||
@Output() openNewRule = new EventEmitter<any>();
|
||||
@Output() replicateManual = new EventEmitter<ReplicationRule>();
|
||||
|
||||
projectScope: boolean = false;
|
||||
|
||||
|
@ -95,8 +96,8 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
|||
setInterval(() => ref.markForCheck(), 500);
|
||||
}
|
||||
|
||||
public get creationAvailable(): boolean {
|
||||
return !this.readonly && this.projectId ? true : false;
|
||||
public get opereateAvailable(): boolean {
|
||||
return !this.readonly && !this.projectId ? true : false;
|
||||
}
|
||||
|
||||
|
||||
|
@ -221,6 +222,10 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
|||
this.editOne.emit(rules);
|
||||
}
|
||||
|
||||
replicateRule(rule: ReplicationRule) {
|
||||
this.replicateManual.emit(rule);
|
||||
}
|
||||
|
||||
toggleRule(rule: ReplicationRule) {
|
||||
let toggleConfirmMessage: ConfirmationMessage = new ConfirmationMessage(
|
||||
rule.enabled === 1 ? 'REPLICATION.TOGGLE_DISABLE_TITLE' : 'REPLICATION.TOGGLE_ENABLE_TITLE',
|
||||
|
|
|
@ -21,4 +21,11 @@ export const REPLICATION_STYLE: string = `
|
|||
.option-right-down {
|
||||
padding-right: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.rightPos{
|
||||
position: absolute;
|
||||
right: 35px;
|
||||
margin-top: 5px;
|
||||
z-index: 100;
|
||||
height: 32px;
|
||||
}`;
|
|
@ -1,13 +1,8 @@
|
|||
export const REPLICATION_TEMPLATE: string = `
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between" style="height:32px; float:right">
|
||||
<div class="row" style="position:relative">
|
||||
<div>
|
||||
<div class="row flex-items-xs-between rightPos">
|
||||
<div class="flex-xs-middle option-right">
|
||||
<div class="select" style="float: left; top: 8px;">
|
||||
<select (change)="doFilterRuleStatus($event)">
|
||||
<option *ngFor="let r of ruleStatus" value="{{r.key}}">{{r.description | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchRules($event)" [currentValue]="search.ruleName"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refreshRules()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
|
@ -16,8 +11,9 @@ export const REPLICATION_TEMPLATE: string = `
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<hbr-list-replication-rule #listReplicationRule [readonly]="readonly" [projectId]="projectId" (selectOne)="selectOneRule($event)" (openNewRule)="openModal()" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)"></hbr-list-replication-rule>
|
||||
<hbr-list-replication-rule #listReplicationRule [readonly]="readonly" [projectId]="projectId" (replicateManual)=replicateManualRule($event) (selectOne)="selectOneRule($event)" (openNewRule)="openModal()" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)"></hbr-list-replication-rule>
|
||||
</div>
|
||||
<br> <br>
|
||||
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between" style="height:60px;">
|
||||
<h5 class="flex-items-xs-bottom option-left-down" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
|
||||
|
@ -72,5 +68,4 @@ export const REPLICATION_TEMPLATE: string = `
|
|||
</clr-datagrid>
|
||||
</div>
|
||||
<job-log-viewer #replicationLogViewer></job-log-viewer>
|
||||
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
|
||||
</div>`;
|
|
@ -260,19 +260,6 @@ describe('Replication Component (inline template)', ()=>{
|
|||
});
|
||||
}));
|
||||
|
||||
it('Should filter replication rules by status', async(()=>{
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(()=>{
|
||||
fixture.detectChanges();
|
||||
comp.doFilterRuleStatus('1' /*Enabled*/);
|
||||
fixture.detectChanges();
|
||||
let el: HTMLElement = deRules.nativeElement;
|
||||
fixture.detectChanges();
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent.trim()).toEqual('sync_02');
|
||||
});
|
||||
}));
|
||||
|
||||
it('Should filter replication jobs by keywords', async(()=>{
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(()=>{
|
||||
|
|
|
@ -88,6 +88,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||
@Input() readonly: boolean;
|
||||
|
||||
@Output() redirect = new EventEmitter<ReplicationRule>();
|
||||
@Output() openCreateRule = new EventEmitter<any>();
|
||||
@Output() openEdit = new EventEmitter<string | number>();
|
||||
|
||||
search: SearchOption = new SearchOption();
|
||||
|
||||
|
@ -111,8 +113,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||
@ViewChild(ListReplicationRuleComponent)
|
||||
listReplicationRule: ListReplicationRuleComponent;
|
||||
|
||||
@ViewChild(CreateEditRuleComponent)
|
||||
createEditPolicyComponent: CreateEditRuleComponent;
|
||||
/* @ViewChild(CreateEditRuleComponent)
|
||||
createEditPolicyComponent: CreateEditRuleComponent;*/
|
||||
|
||||
@ViewChild("replicationLogViewer")
|
||||
replicationLogViewer: JobLogViewerComponent;
|
||||
|
@ -134,9 +136,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||
private translateService: TranslateService) {
|
||||
}
|
||||
|
||||
public get creationAvailable(): boolean {
|
||||
return !this.readonly && this.projectId ? true : false;
|
||||
}
|
||||
|
||||
public get showPaginationIndex(): boolean {
|
||||
return this.totalCount > 0;
|
||||
|
@ -155,7 +154,7 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
openModal(): void {
|
||||
this.createEditPolicyComponent.openCreateEditRule(true);
|
||||
this.openCreateRule.emit();
|
||||
}
|
||||
|
||||
openEditRule(rule: ReplicationRule) {
|
||||
|
@ -164,7 +163,7 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||
if (rule.enabled === 1) {
|
||||
editable = false;
|
||||
}
|
||||
this.createEditPolicyComponent.openCreateEditRule(editable, rule.id);
|
||||
this.openEdit.emit(rule.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,6 +259,14 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
replicateManualRule(rule: ReplicationRule): void {
|
||||
toPromise<any>(this.replicationService.replicateRule(rule.id))
|
||||
.then(response => {
|
||||
this.refreshJobs();
|
||||
})
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
}
|
||||
|
||||
customRedirect(rule: ReplicationRule) {
|
||||
this.redirect.emit(rule);
|
||||
}
|
||||
|
@ -269,14 +276,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
|||
this.listReplicationRule.retrieveRules(ruleName);
|
||||
}
|
||||
|
||||
doFilterRuleStatus($event: any) {
|
||||
if ($event && $event.target && $event.target["value"]) {
|
||||
let status = $event.target["value"];
|
||||
this.currentRuleStatus = this.ruleStatus.find((r: any) => r.key === status);
|
||||
this.listReplicationRule.filterRuleStatus(this.currentRuleStatus.key);
|
||||
}
|
||||
}
|
||||
|
||||
doFilterJobStatus($event: any) {
|
||||
if ($event && $event.target && $event.target["value"]) {
|
||||
let status = $event.target["value"];
|
||||
|
|
|
@ -26,8 +26,25 @@ export const REPOSITORY_STYLE = `.option-right {
|
|||
font-size: 32px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
.no-info-div {
|
||||
background: white;
|
||||
border: 1px;
|
||||
border-style: solid;
|
||||
border-color: #CCCCCC;
|
||||
padding: 12px 12px 12px 12px;
|
||||
}
|
||||
|
||||
.info-div {
|
||||
background: white;
|
||||
border: 1px;
|
||||
border-style: solid;
|
||||
border-color: #CCCCCC;
|
||||
padding: 0px 12px 24px 12px;
|
||||
}
|
||||
|
||||
.info-pre {
|
||||
border: 0px;
|
||||
max-height: fit-content;
|
||||
}
|
||||
|
||||
#info-edit-button {
|
||||
|
|
|
@ -24,12 +24,18 @@ export const REPOSITORY_TEMPLATE = `
|
|||
<section id="info" role="tabpanel" aria-labelledby="repo-info" [hidden]='!isCurrentTabContent("info")'>
|
||||
<form #repoInfoForm="ngForm">
|
||||
<div id="info-edit-button">
|
||||
<button class="btn btn-sm" [disabled]="editing" (click)="editInfo()" >{{'BUTTON.EDIT' | translate}}</button>
|
||||
<button class="btn btn-sm" [disabled]="editing || !hasProjectAdminRole " (click)="editInfo()" >{{'BUTTON.EDIT' | translate}}</button>
|
||||
</div>
|
||||
<div>
|
||||
<h3 *ngIf="!editing && !hasInfo()" >{{'REPOSITORY.NO_INFO' | translate }}</h3>
|
||||
<pre *ngIf="!editing && hasInfo()" ><code>{{ imageInfo }}</code></pre>
|
||||
<textarea *ngIf="editing" name="info-edit-textarea" [(ngModel)]="imageInfo"></textarea>
|
||||
<div *ngIf="!editing">
|
||||
<div *ngIf="!hasInfo()" class="no-info-div">
|
||||
<p>{{'REPOSITORY.NO_INFO' | translate }}<p>
|
||||
</div>
|
||||
<div *ngIf="hasInfo()" class="info-div">
|
||||
<pre class="info-pre">{{ imageInfo }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="editing">
|
||||
<textarea rows="5" name="info-edit-textarea" [(ngModel)]="imageInfo"></textarea>
|
||||
</div>
|
||||
<div class="btn-sm" *ngIf="editing">
|
||||
<button class="btn btn-primary" [disabled]="!hasChanges()" (click)="saveInfo()" >{{'BUTTON.SAVE' | translate}}</button>
|
||||
|
|
|
@ -97,6 +97,9 @@ export abstract class ReplicationService {
|
|||
*/
|
||||
abstract disableReplicationRule(ruleId: number | string): Observable<any> | Promise<any> | any;
|
||||
|
||||
|
||||
abstract replicateRule(ruleId: number | string): Observable<any> | Promise<any> | any;
|
||||
|
||||
/**
|
||||
* Get the jobs for the specified replication rule.
|
||||
* Set query parameters through 'queryParams', support:
|
||||
|
@ -137,6 +140,7 @@ export abstract class ReplicationService {
|
|||
export class ReplicationDefaultService extends ReplicationService {
|
||||
_ruleBaseUrl: string;
|
||||
_jobBaseUrl: string;
|
||||
_replicateUrl: string;
|
||||
|
||||
constructor(
|
||||
private http: Http,
|
||||
|
@ -147,6 +151,7 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||
config.replicationRuleEndpoint : '/api/policies/replication';
|
||||
this._jobBaseUrl = config.replicationJobEndpoint ?
|
||||
config.replicationJobEndpoint : '/api/jobs/replication';
|
||||
this._replicateUrl = '/api/replications';
|
||||
}
|
||||
|
||||
//Private methods
|
||||
|
@ -216,6 +221,17 @@ export class ReplicationDefaultService extends ReplicationService {
|
|||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public replicateRule(ruleId: number | string): Observable<any> | Promise<any> | any {
|
||||
if (!ruleId) {
|
||||
return Promise.reject("Bad argument");
|
||||
}
|
||||
|
||||
let url: string = `${this._replicateUrl}`;
|
||||
return this.http.post(url, {policy_id: ruleId}, HTTP_JSON_OPTIONS).toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public enableReplicationRule(ruleId: number | string, enablement: number): Observable<any> | Promise<any> | any {
|
||||
if (!ruleId || ruleId <= 0) {
|
||||
return Promise.reject('Bad argument');
|
||||
|
|
|
@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
|||
import { HttpModule, Http } from '@angular/http';
|
||||
import { ClarityModule } from 'clarity-angular';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from "@ngx-translate/core";
|
||||
import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from '@ngx-translate/core';
|
||||
import { MyMissingTranslationHandler } from '../i18n/missing-trans.handler';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { TranslatorJsonLoader } from '../i18n/local-json.loader';
|
||||
|
@ -30,9 +30,9 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* Module for sharing common modules
|
||||
*
|
||||
*
|
||||
* @export
|
||||
* @class SharedModule
|
||||
*/
|
||||
|
@ -68,4 +68,4 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
|
|||
providers: [CookieService]
|
||||
})
|
||||
|
||||
export class SharedModule { }
|
||||
export class SharedModule { }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "harbor",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"description": "Harbor UI with Clarity",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
|
@ -31,7 +31,7 @@
|
|||
"clarity-icons": "^0.10.17",
|
||||
"clarity-ui": "^0.10.17",
|
||||
"core-js": "^2.4.1",
|
||||
"harbor-ui": "0.6.5",
|
||||
"harbor-ui": "0.6.9",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
|
|
|
@ -21,7 +21,7 @@ export default {
|
|||
plugins: [
|
||||
nodeResolve({jsnext: true, module: true, browser: true}),
|
||||
commonjs({
|
||||
include: ['node_modules/**']
|
||||
include: ['node_modules/**'],
|
||||
}),
|
||||
uglify()
|
||||
]
|
||||
|
|
|
@ -47,4 +47,5 @@
|
|||
font-size: 14px !important;
|
||||
position: relative;
|
||||
top: -9px;
|
||||
}
|
||||
}
|
||||
.bg{position: absolute;top: 60px; right: 0px;width: 100%; height: 100%; background-size: cover;}
|
|
@ -1,4 +1,5 @@
|
|||
<div class="login-wrapper login-wrapper-override" [ngStyle]="{'background-image': 'url(' + customLoginBgImg + ')'}">
|
||||
<div class="login-wrapper login-wrapper-override">
|
||||
<div class="bg" *ngIf="customLoginBgImg" [ngStyle]="{'background-image': 'url(static/images/' + customLoginBgImg + ')'}"></div>
|
||||
<form #signInForm="ngForm" class="login">
|
||||
<label class="title"> {{customAppTitle? customAppTitle:(appTitle | translate)}}<span class="trademark tm-font">™</span>
|
||||
</label>
|
||||
|
|
|
@ -57,7 +57,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
|
|||
|
||||
ngOnInit(): void {
|
||||
//custom skin
|
||||
let customSkinObj = this.skinableConfig.getProjects();
|
||||
let customSkinObj = this.skinableConfig.getProject();
|
||||
if (customSkinObj && customSkinObj.projectName) {
|
||||
this.translate.get('GLOBAL_SEARCH.PLACEHOLDER', {'param': customSkinObj.projectName}).subscribe(res => {
|
||||
//Placeholder text
|
||||
|
|
|
@ -8,21 +8,47 @@
|
|||
<search-result></search-result>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
<nav class="sidenav" style="padding: 12px 36px;" *ngIf="isUserExisting">
|
||||
<section class="sidenav-content" style="padding-top: 20px;">
|
||||
<a routerLink="/harbor/projects" routerLinkActive="active" class="nav-link nav-link-override">{{'SIDE_NAV.PROJECTS' | translate}}</a>
|
||||
<a routerLink="/harbor/logs" routerLinkActive="active" class="nav-link nav-link-override" style="margin-top: 4px;">{{'SIDE_NAV.LOGS' | translate}}</a>
|
||||
<section class="nav-group collapsible" *ngIf="isSystemAdmin" style="margin-top: 4px;">
|
||||
<input id="tabsystem" type="checkbox">
|
||||
<label for="tabsystem">{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}</label>
|
||||
<ul class="nav-list">
|
||||
<li><a class="nav-link nav-link-override" routerLink="/harbor/users" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}</a></li>
|
||||
<li><a class="nav-link nav-link-override" routerLink="/harbor/replications" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</a></li>
|
||||
<li><a class="nav-link nav-link-override" routerLink="/harbor/configs" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.CONFIG' | translate}}</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
</nav>
|
||||
<clr-vertical-nav [clrVerticalNavCollapsible]="true" *ngIf="isUserExisting">
|
||||
<a clrVerticalNavLink routerLinkActive="active" routerLink="/harbor/projects">
|
||||
<clr-icon shape="organization" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.PROJECTS' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink routerLinkActive="active" routerLink="/harbor/logs">
|
||||
<clr-icon shape="list" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.LOGS' | translate}}
|
||||
</a>
|
||||
<clr-vertical-nav-group *ngIf="isSystemAdmin" routerLinkActive="active">
|
||||
<clr-icon shape="administrator" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.NAME' | translate}}
|
||||
<a routerLink="#" hidden aria-hidden="true"></a>
|
||||
<clr-vertical-nav-group-children *clrIfExpanded="true">
|
||||
<a clrVerticalNavLink
|
||||
routerLink="/harbor/users"
|
||||
routerLinkActive="active">
|
||||
<clr-icon shape="users" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink
|
||||
routerLink="/harbor/registries"
|
||||
routerLinkActive="active">
|
||||
<clr-icon shape="block" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink
|
||||
routerLink="/harbor/replications"
|
||||
routerLinkActive="active">
|
||||
<clr-icon shape="cloud-traffic" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}
|
||||
</a>
|
||||
<a clrVerticalNavLink
|
||||
routerLink="/harbor/configs"
|
||||
routerLinkActive="active">
|
||||
<clr-icon shape="cog" clrVerticalNavIcon></clr-icon>
|
||||
{{'SIDE_NAV.SYSTEM_MGMT.CONFIG' | translate}}
|
||||
</a>
|
||||
</clr-vertical-nav-group-children>
|
||||
</clr-vertical-nav-group>
|
||||
</clr-vertical-nav>
|
||||
</div>
|
||||
</clr-main-container>
|
||||
<account-settings-modal></account-settings-modal>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="branding">
|
||||
<a href="javascript:void(0)" class="nav-link" (click)="homeAction()">
|
||||
<clr-icon shape="vm-bug" *ngIf="!customStyle?.headerLogo"></clr-icon>
|
||||
<img [attr.src]="customStyle?.headerLogo" *ngIf="customStyle?.headerLogo" style="width: 36px;height: 36px; object-fit: fill;">
|
||||
<img [attr.src]="'static/images/'+customStyle?.headerLogo" *ngIf="customStyle?.headerLogo" style="width: 36px;height: 36px; object-fit: fill;">
|
||||
<span class="title">{{customProjectName?.projectName? customProjectName?.projectName:(appTitle | translate)}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -50,6 +50,8 @@ import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deac
|
|||
import { MemberGuard } from './shared/route/member-guard-activate.service';
|
||||
|
||||
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
|
||||
import { ReplicationRuleComponent} from "./replication/replication-rule/replication-rule.component";
|
||||
import {LeavingNewRuleRouteDeactivate} from "./shared/route/leaving-new-rule-deactivate.service";
|
||||
|
||||
const harborRoutes: Routes = [
|
||||
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
|
||||
|
@ -78,25 +80,30 @@ const harborRoutes: Routes = [
|
|||
component: UserComponent,
|
||||
canActivate: [SystemAdminGuard]
|
||||
},
|
||||
{
|
||||
path: 'registries',
|
||||
component: DestinationPageComponent,
|
||||
canActivate: [SystemAdminGuard]
|
||||
},
|
||||
{
|
||||
path: 'replications',
|
||||
component: ReplicationManagementComponent,
|
||||
component: TotalReplicationPageComponent,
|
||||
canActivate: [SystemAdminGuard],
|
||||
canActivateChild: [SystemAdminGuard],
|
||||
children: [
|
||||
{
|
||||
path: 'rules',
|
||||
component: TotalReplicationPageComponent
|
||||
},
|
||||
{
|
||||
path: 'endpoints',
|
||||
component: DestinationPageComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'endpoints'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'replications/:id/rule',
|
||||
component: ReplicationRuleComponent,
|
||||
canActivate: [SystemAdminGuard],
|
||||
canActivateChild: [SystemAdminGuard],
|
||||
canDeactivate: [LeavingNewRuleRouteDeactivate]
|
||||
},
|
||||
{
|
||||
path: 'replications/new-rule',
|
||||
component: ReplicationRuleComponent,
|
||||
canActivate: [SystemAdminGuard],
|
||||
canActivateChild: [SystemAdminGuard],
|
||||
canDeactivate: [LeavingNewRuleRouteDeactivate]
|
||||
},
|
||||
{
|
||||
path: 'tags/:id/:repo',
|
||||
|
@ -137,7 +144,6 @@ const harborRoutes: Routes = [
|
|||
{
|
||||
path: 'replications',
|
||||
component: ReplicationPageComponent,
|
||||
canActivate: [SystemAdminGuard]
|
||||
},
|
||||
{
|
||||
path: 'members',
|
||||
|
@ -158,6 +164,12 @@ const harborRoutes: Routes = [
|
|||
component: ConfigurationComponent,
|
||||
canActivate: [SystemAdminGuard],
|
||||
canDeactivate: [LeavingConfigRouteDeactivate]
|
||||
},
|
||||
{
|
||||
path: 'registry',
|
||||
component: DestinationPageComponent,
|
||||
canActivate: [SystemAdminGuard],
|
||||
canActivateChild: [SystemAdminGuard],
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
|
||||
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
|
||||
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSessionValid && (isSystemAdmin || isMember)">
|
||||
|
|
|
@ -50,7 +50,11 @@ export class ProjectDetailComponent {
|
|||
|
||||
public get isSystemAdmin(): boolean {
|
||||
let account = this.sessionService.getCurrentUser();
|
||||
return account != null && account.has_admin_role > 0;
|
||||
return account && account.has_admin_role > 0;
|
||||
}
|
||||
|
||||
public get isSProjectAdmin(): boolean {
|
||||
return this.currentProject.has_project_admin_role;
|
||||
}
|
||||
|
||||
public get isSessionValid(): boolean {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}}</h2>
|
||||
<div style="margin-top: 24px;">
|
||||
<hbr-endpoint></hbr-endpoint>
|
||||
</div>
|
|
@ -11,6 +11,10 @@
|
|||
// 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.
|
||||
|
||||
// This Module is used as Container For Endpoint and Replication Rules
|
||||
// Will deprecated on Harbor 1.4.0
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<div style="margin-top: 24px;">
|
||||
<hbr-replication #replicationView [projectId]="projectIdentify" [withReplicationJob]='true'></hbr-replication>
|
||||
<hbr-replication [readonly]="true" #replicationView [projectId]="projectIdentify" [withReplicationJob]='true'></hbr-replication>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
.datagrid .datagrid-head{border: 0;}
|
||||
.option-right{ position: absolute; right: 30px; top: 55px;}
|
||||
:host >>> .datagrid-head{height: 0;border-width: 0;}
|
||||
:host >>> .datagrid-scroll-wrapper .datagrid{margin-top: 0;}
|
||||
.modal-body{height: 30em; overflow-y: auto; margin-top: 20px;}
|
|
@ -0,0 +1,33 @@
|
|||
<clr-modal [(clrModalOpen)]="ismodelOpen">
|
||||
<h3 class="modal-title">{{'PROJECT.ALL_PROJECTS' | translate}}</h3>
|
||||
<inline-alert class="modal-title" ></inline-alert>
|
||||
<div class="modal-body">
|
||||
<div class="option-right">
|
||||
<div class="select" style="float: left; left:-6px; top:8px;">
|
||||
<select (change)="doFilterProject()" [(ngModel)]="selecteType">
|
||||
<option value="0" [selected]="currentFilteredType === 0">{{projectTypes[0] | translate}}</option>
|
||||
<option value="1">{{projectTypes[1] | translate}}</option>
|
||||
<option value="2">{{projectTypes[2] | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchProject($event)" [currentValue]="projectName"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedProject">
|
||||
<clr-dg-row *clrDgItems="let project of projects; let i = index" [clrDgItem]="project">
|
||||
<clr-dg-cell>{{project.name}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} </span> {{pagination.totalItems }} {{'PROJECT.ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="closeModel()">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" [disabled]="!selectedProject" (click)="oKModel()">{{'BUTTON.OK' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
|
@ -0,0 +1,180 @@
|
|||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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.
|
||||
import {
|
||||
Component,
|
||||
Output,
|
||||
Input,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
OnDestroy, EventEmitter
|
||||
} from '@angular/core';
|
||||
import { Router, NavigationExtras } from '@angular/router';
|
||||
|
||||
import { SessionService } from '../../../shared/session.service';
|
||||
import { SearchTriggerService } from '../../../base/global-search/search-trigger.service';
|
||||
import { ProjectTypes, RoleInfo} from '../../../shared/shared.const';
|
||||
import { CustomComparator, doFiltering, doSorting, calculatePage } from '../../../shared/shared.utils';
|
||||
|
||||
import { Comparator, State } from 'clarity-angular';
|
||||
import { MessageHandlerService } from '../../../shared/message-handler/message-handler.service';
|
||||
import { StatisticHandler } from '../../../shared/statictics/statistic-handler.service';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service';
|
||||
import { ConfirmationMessage } from '../../../shared/confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../../shared/shared.const';
|
||||
import {ProjectService} from "../../../project/project.service";
|
||||
import {Project} from "../../../project/project";
|
||||
|
||||
@Component({
|
||||
selector: 'list-project-model',
|
||||
templateUrl: 'list-project-model.component.html',
|
||||
styleUrls: ['list-project-model.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ListProjectModelComponent {
|
||||
projectTypes = ProjectTypes;
|
||||
loading: boolean = true;
|
||||
projects: Project[] = [];
|
||||
filteredType: number = 0;//All projects
|
||||
searchKeyword: string = "";
|
||||
ismodelOpen: boolean ;
|
||||
currentFilteredType: number = 0;//all projects
|
||||
projectName: string = "";
|
||||
selectedProject: Project;
|
||||
|
||||
roleInfo = RoleInfo;
|
||||
repoCountComparator: Comparator<Project> = new CustomComparator<Project>("repo_count", "number");
|
||||
timeComparator: Comparator<Project> = new CustomComparator<Project>("creation_time", "date");
|
||||
accessLevelComparator: Comparator<Project> = new CustomComparator<Project>("public", "number");
|
||||
roleComparator: Comparator<Project> = new CustomComparator<Project>("current_user_role_id", "number");
|
||||
currentPage: number = 1;
|
||||
totalCount: number = 0;
|
||||
pageSize: number = 10;
|
||||
currentState: State;
|
||||
@Output() selectedPro = new EventEmitter<Project>();
|
||||
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private router: Router,
|
||||
private searchTrigger: SearchTriggerService,
|
||||
private proService: ProjectService,
|
||||
private msgHandler: MessageHandlerService,
|
||||
private statisticHandler: StatisticHandler,
|
||||
private deletionDialogService: ConfirmationDialogService,
|
||||
private ref: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
get selecteType(): number {
|
||||
return this.currentFilteredType;
|
||||
}
|
||||
set selecteType(_project: number) {
|
||||
this.currentFilteredType = _project;
|
||||
if (window.sessionStorage) {
|
||||
window.sessionStorage['projectTypeValue'] = _project;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
clrLoad(state: State) {
|
||||
//Keep state for future filtering and sorting
|
||||
this.currentState = state;
|
||||
|
||||
let pageNumber: number = calculatePage(state);
|
||||
if (pageNumber <= 0) { pageNumber = 1; }
|
||||
|
||||
this.loading = true;
|
||||
|
||||
let passInFilteredType: number = undefined;
|
||||
if (this.filteredType > 0) {
|
||||
passInFilteredType = this.filteredType - 1;
|
||||
}
|
||||
this.proService.listProjects(this.searchKeyword, passInFilteredType, pageNumber, this.pageSize).toPromise()
|
||||
.then(response => {
|
||||
//Get total count
|
||||
if (response.headers) {
|
||||
let xHeader: string = response.headers.get("X-Total-Count");
|
||||
if (xHeader) {
|
||||
this.totalCount = parseInt(xHeader, 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.projects = response.json() as Project[];
|
||||
//Do customising filtering and sorting
|
||||
this.projects = doFiltering<Project>(this.projects, state);
|
||||
this.projects = doSorting<Project>(this.projects, state);
|
||||
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.loading = false;
|
||||
this.msgHandler.handleError(error);
|
||||
});
|
||||
|
||||
//Force refresh view
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 3000);
|
||||
}
|
||||
|
||||
openModel(): void {
|
||||
this.selectedProject = null;
|
||||
this.ismodelOpen = true;
|
||||
//Force refresh view
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 2000);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.currentPage = 1;
|
||||
this.filteredType = 0;
|
||||
this.searchKeyword = '';
|
||||
|
||||
this.reload();
|
||||
}
|
||||
|
||||
doFilterProject(): void {
|
||||
this.currentPage = 1;
|
||||
this.filteredType = this.selecteType;
|
||||
this.reload();
|
||||
}
|
||||
|
||||
doSearchProject(proName: string): void {
|
||||
this.projectName = proName;
|
||||
this.currentPage = 1;
|
||||
this.searchKeyword = proName;
|
||||
this.reload();
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
let st: State = this.currentState;
|
||||
if (!st) {
|
||||
st = {
|
||||
page: {}
|
||||
};
|
||||
}
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
st.page.size = this.pageSize;
|
||||
|
||||
this.clrLoad(st);
|
||||
}
|
||||
|
||||
oKModel() {
|
||||
this.ismodelOpen = false;
|
||||
this.selectedPro.emit(this.selectedProject);
|
||||
}
|
||||
|
||||
closeModel(): void {
|
||||
this.ismodelOpen = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,531 @@
|
|||
import {Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, AfterViewInit} from '@angular/core';
|
||||
import {ProjectService} from '../../project/project.service';
|
||||
import {Project} from '../../project/project';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {FormArray, FormBuilder, FormGroup, Validators} from "@angular/forms";
|
||||
import {ReplicationRuleServie} from "./replication-rule.service";
|
||||
import {MessageHandlerService} from "../../shared/message-handler/message-handler.service";
|
||||
import {Target, Filter, ReplicationRule} from "./replication-rule";
|
||||
import {ConfirmationDialogService} from "../../shared/confirmation-dialog/confirmation-dialog.service";
|
||||
import { ConfirmationTargets, ConfirmationState } from '../../shared/shared.const';
|
||||
import {Subscription} from "rxjs/Subscription";
|
||||
import {ConfirmationMessage} from "../../shared/confirmation-dialog/confirmation-message";
|
||||
import {Subject} from "rxjs/Subject";
|
||||
import {ListProjectModelComponent} from "./list-project-model/list-project-model.component";
|
||||
import {toPromise, isEmptyObject, compareValue} from "harbor-ui/src/utils";
|
||||
|
||||
|
||||
const ONE_HOUR_SECONDS: number = 3600;
|
||||
const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
|
||||
|
||||
@Component ({
|
||||
selector: 'repliction-rule',
|
||||
templateUrl: 'replication-rule.html',
|
||||
styleUrls: ['replication-rule.css']
|
||||
|
||||
})
|
||||
export class ReplicationRuleComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
_localTime: Date = new Date();
|
||||
policyId: number;
|
||||
targetList: Target[] = [];
|
||||
isFilterHide: boolean = false;
|
||||
weeklySchedule: boolean;
|
||||
isScheduleOpt: boolean;
|
||||
isImmediate: boolean = true;
|
||||
filterCount: number = 0;
|
||||
selectedprojectList: Project[] = [];
|
||||
triggerNames: string[] = ['immediate', 'schedule', 'manual'];
|
||||
scheduleNames: string[] = ['daily', 'weekly'];
|
||||
weekly: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||
filterSelect: string[] = ['repository', 'tag'];
|
||||
ruleNameTooltip: string = 'TOOLTIP.EMPTY';
|
||||
headerTitle: string = 'REPLICATION.ADD_POLICY';
|
||||
|
||||
filterListData: {[key: string]: any}[] = [];
|
||||
inProgress: boolean = false;
|
||||
inNameChecking: boolean = false;
|
||||
isRuleNameExist: boolean = false;
|
||||
nameChecker: Subject<string> = new Subject<string>();
|
||||
|
||||
confirmSub: Subscription;
|
||||
ruleForm: FormGroup;
|
||||
copyUpdateForm: ReplicationRule;
|
||||
|
||||
@ViewChild(ListProjectModelComponent)
|
||||
projectListModel: ListProjectModelComponent;
|
||||
|
||||
baseFilterData(name: string, option: string[], state: boolean) {
|
||||
return {
|
||||
name: name,
|
||||
options: option,
|
||||
state: state,
|
||||
isValid: true
|
||||
};
|
||||
}
|
||||
|
||||
constructor(public projectService: ProjectService,
|
||||
private router: Router,
|
||||
private fb: FormBuilder,
|
||||
private repService: ReplicationRuleServie,
|
||||
private route: ActivatedRoute,
|
||||
private msgHandler: MessageHandlerService,
|
||||
private confirmService: ConfirmationDialogService,
|
||||
public ref: ChangeDetectorRef) {
|
||||
this.createForm();
|
||||
|
||||
Promise.all([this.repService.getEndpoints(), this.repService.listProjects()])
|
||||
.then(res => {
|
||||
if (!res[0] || !res[1]) {
|
||||
this.msgHandler.error('REPLICATION.BACKINFO');
|
||||
setTimeout(() => {
|
||||
this.router.navigate(['/harbor/replications']);
|
||||
}, 2000);
|
||||
};
|
||||
if (res[0] && res[1]) {
|
||||
this.targetList = res[0];
|
||||
if (!this.policyId) {
|
||||
this.setTarget([res[0][0]]);
|
||||
this.setProject([res[1][0]]);
|
||||
this.copyUpdateForm = Object.assign({}, this.ruleForm.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.policyId = +this.route.snapshot.params['id'];
|
||||
if (this.policyId) {
|
||||
this.headerTitle = 'REPLICATION.EDIT_POLICY_TITLE';
|
||||
this.repService.getReplicationRule(this.policyId)
|
||||
.then((response) => {
|
||||
this.copyUpdateForm = Object.assign({}, response);
|
||||
// set filter value is [] if callback fiter value is null.
|
||||
this.copyUpdateForm.filters = response.filters ? response.filters : [];
|
||||
this.updateForm(response);
|
||||
}).catch(error => {
|
||||
this.msgHandler.handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((ruleName: string) => {
|
||||
this.isRuleNameExist = false;
|
||||
this.inNameChecking = true;
|
||||
toPromise<ReplicationRule[]>(this.repService.getReplicationRules(0, ruleName))
|
||||
.then(response => {
|
||||
if (response.some(rule => rule.name === ruleName)) {
|
||||
this.ruleNameTooltip = 'TOOLTIP.RULE_USER_EXISTING';
|
||||
this.isRuleNameExist = true;
|
||||
}
|
||||
this.inNameChecking = false;
|
||||
}).catch(() => {
|
||||
this.inNameChecking = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.confirmSub) {
|
||||
this.confirmSub.unsubscribe();
|
||||
}
|
||||
if (this.nameChecker) {
|
||||
this.nameChecker.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
createForm() {
|
||||
this.ruleForm = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
description: '',
|
||||
projects: this.fb.array([]),
|
||||
targets: this.fb.array([]),
|
||||
trigger: this.fb.group({
|
||||
kind: this.triggerNames[0],
|
||||
schedule_param: this.fb.group({
|
||||
type: this.scheduleNames[0],
|
||||
weekday: 1,
|
||||
offtime: '08:00'
|
||||
}),
|
||||
}),
|
||||
filters: this.fb.array([]),
|
||||
replicate_existing_image_now: true,
|
||||
replicate_deletion: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
updateForm(rule: ReplicationRule): void {
|
||||
rule.trigger = this.updateTrigger(rule.trigger);
|
||||
this.ruleForm.reset({
|
||||
name: rule.name,
|
||||
description: rule.description,
|
||||
trigger: rule.trigger,
|
||||
replicate_existing_image_now: rule.replicate_existing_image_now,
|
||||
replicate_deletion: rule.replicate_deletion
|
||||
});
|
||||
this.setProject(rule.projects);
|
||||
this.setTarget(rule.targets);
|
||||
if (rule.filters) {
|
||||
this.setFilter(rule.filters);
|
||||
this.updateFilter(rule.filters);
|
||||
}
|
||||
|
||||
// Force refresh view
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 2000);
|
||||
}
|
||||
|
||||
get projects(): FormArray {
|
||||
return this.ruleForm.get('projects') as FormArray;
|
||||
}
|
||||
setProject(projects: Project[]) {
|
||||
const projectFGs = projects.map(project => this.fb.group(project));
|
||||
const projectFormArray = this.fb.array(projectFGs);
|
||||
this.ruleForm.setControl('projects', projectFormArray);
|
||||
}
|
||||
|
||||
get filters(): FormArray {
|
||||
return this.ruleForm.get('filters') as FormArray;
|
||||
}
|
||||
setFilter(filters: Filter[]) {
|
||||
const filterFGs = filters.map(filter => this.fb.group(filter));
|
||||
const filterFormArray = this.fb.array(filterFGs);
|
||||
this.ruleForm.setControl('filters', filterFormArray);
|
||||
}
|
||||
|
||||
get targets(): FormArray {
|
||||
return this.ruleForm.get('targets') as FormArray;
|
||||
}
|
||||
setTarget(targets: Target[]) {
|
||||
const targetFGs = targets.map(target => this.fb.group(target));
|
||||
const targetFormArray = this.fb.array(targetFGs);
|
||||
this.ruleForm.setControl('targets', targetFormArray);
|
||||
}
|
||||
|
||||
initFilter(name: string) {
|
||||
return this.fb.group({
|
||||
kind: name,
|
||||
pattern: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
filterChange($event: any) {
|
||||
if ($event && $event.target['value']) {
|
||||
let id: number = $event.target.id;
|
||||
let name: string = $event.target.name;
|
||||
let value: string = $event.target['value'];
|
||||
|
||||
this.filterListData.forEach((data, index) => {
|
||||
if (index === +id) {
|
||||
data.name = $event.target.name = value;
|
||||
}else {
|
||||
data.options.splice(data.options.indexOf(value), 1);
|
||||
}
|
||||
if (data.options.indexOf(name) === -1) {
|
||||
data.options.push(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
targetChange($event: any) {
|
||||
if ($event && $event.target && event.target['value']) {
|
||||
let selecedTarget: Target = this.targetList.find(target => target.id === +$event.target['value']);
|
||||
this.setTarget([selecedTarget]);
|
||||
}
|
||||
}
|
||||
|
||||
openProjectModel(): void {
|
||||
this.projectListModel.openModel();
|
||||
}
|
||||
|
||||
selectedProject(project: Project): void {
|
||||
this.setProject([project]);
|
||||
}
|
||||
|
||||
addNewFilter(): void {
|
||||
if (this.filterCount === 0) {
|
||||
this.filterListData.push(this.baseFilterData(this.filterSelect[0], this.filterSelect.slice(), true));
|
||||
this.filters.push(this.initFilter(this.filterSelect[0]));
|
||||
|
||||
}else {
|
||||
let nameArr: string[] = this.filterSelect.slice();
|
||||
this.filterListData.forEach(data => {
|
||||
nameArr.splice(nameArr.indexOf(data.name), 1);
|
||||
});
|
||||
// when add a new filter,the filterListData should change the options
|
||||
this.filterListData.filter((data) => {
|
||||
data.options.splice(data.options.indexOf(nameArr[0]), 1);
|
||||
});
|
||||
this.filterListData.push(this.baseFilterData(nameArr[0], nameArr, true));
|
||||
this.filters.push(this.initFilter(nameArr[0]));
|
||||
}
|
||||
this.filterCount += 1;
|
||||
if (this.filterCount >= this.filterSelect.length) {
|
||||
this.isFilterHide = true;
|
||||
}
|
||||
}
|
||||
|
||||
// delete a filter
|
||||
deleteFilter(i: number): void {
|
||||
if (i || i === 0) {
|
||||
let delfilter = this.filterListData.splice(i, 1)[0];
|
||||
if (this.filterCount === this.filterSelect.length) {
|
||||
this.isFilterHide = false;
|
||||
}
|
||||
this.filterCount -= 1;
|
||||
if (this.filterListData.length) {
|
||||
let optionVal = delfilter.name;
|
||||
this.filterListData.filter(data => {
|
||||
if (data.options.indexOf(optionVal) === -1) {
|
||||
data.options.push(optionVal);
|
||||
}
|
||||
});
|
||||
}
|
||||
const control = <FormArray>this.ruleForm.controls['filters'];
|
||||
control.removeAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
selectTrigger($event: any): void {
|
||||
if ($event && $event.target && $event.target['value']) {
|
||||
let val: string = $event.target['value'];
|
||||
if (val === this.triggerNames[1]) {
|
||||
this.isScheduleOpt = true;
|
||||
this.isImmediate = false;
|
||||
}
|
||||
if (val === this.triggerNames[0]) {
|
||||
this.isScheduleOpt = false;
|
||||
this.isImmediate = true;
|
||||
}
|
||||
if (val === this.triggerNames[2]) {
|
||||
this.isScheduleOpt = false;
|
||||
this.isImmediate = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replication Schedule select value exchange
|
||||
selectSchedule($event: any): void {
|
||||
if ($event && $event.target && $event.target['value']) {
|
||||
switch ($event.target['value']) {
|
||||
case this.scheduleNames[1]:
|
||||
this.weeklySchedule = true;
|
||||
this.ruleForm.patchValue({
|
||||
trigger: {
|
||||
schedule_param: {
|
||||
weekday: 1,
|
||||
}
|
||||
}
|
||||
})
|
||||
break;
|
||||
case this.scheduleNames[0]:
|
||||
this.weeklySchedule = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkRuleName(): void {
|
||||
let ruleName: string = this.ruleForm.controls['name'].value;
|
||||
if (ruleName) {
|
||||
this.nameChecker.next(ruleName);
|
||||
} else {
|
||||
this.ruleNameTooltip = 'TOOLTIP.EMPTY';
|
||||
}
|
||||
}
|
||||
|
||||
updateFilter(filters: any) {
|
||||
let opt: string[] = this.filterSelect.slice();
|
||||
filters.forEach((filter: any) => {
|
||||
opt.splice(opt.indexOf(filter.kind), 1);
|
||||
})
|
||||
filters.forEach((filter: any) => {
|
||||
let option: string [] = opt.slice();
|
||||
option.unshift(filter.kind);
|
||||
this.filterListData.push(this.baseFilterData(filter.kind, option, true));
|
||||
});
|
||||
this.filterCount = filters.length;
|
||||
if (filters.length === this.filterSelect.length) {
|
||||
this.isFilterHide = true;
|
||||
}
|
||||
}
|
||||
|
||||
updateTrigger(trigger: any) {
|
||||
if (trigger['schedule_param']) {
|
||||
this.isScheduleOpt = true;
|
||||
this.isImmediate = false;
|
||||
trigger['schedule_param']['offtime'] = this.getOfftime(trigger['schedule_param']['offtime']);
|
||||
if (trigger['schedule_param']['weekday']) {
|
||||
this.weeklySchedule = true;
|
||||
}else {
|
||||
// set default
|
||||
trigger['schedule_param']['weekday'] = 1;
|
||||
}
|
||||
}else {
|
||||
if (trigger['kind'] === this.triggerNames[2]) {
|
||||
this.isImmediate = false;
|
||||
}
|
||||
trigger['schedule_param'] = { type: this.scheduleNames[0],
|
||||
weekday: this.weekly[0],
|
||||
offtime: '08:00'};
|
||||
}
|
||||
return trigger;
|
||||
}
|
||||
|
||||
setTriggerVaule(trigger: any) {
|
||||
if (!this.isScheduleOpt) {
|
||||
delete trigger['schedule_param'];
|
||||
return trigger;
|
||||
}else {
|
||||
if (!this.weeklySchedule) {
|
||||
delete trigger['schedule_param']['weekday'];
|
||||
}else {
|
||||
trigger['schedule_param']['weekday'] = +trigger['schedule_param']['weekday'];
|
||||
}
|
||||
trigger['schedule_param']['offtime'] = this.setOfftime(trigger['schedule_param']['offtime']);
|
||||
return trigger;
|
||||
}
|
||||
}
|
||||
|
||||
public hasFormChange(): boolean {
|
||||
return !isEmptyObject(this.getChanges());
|
||||
}
|
||||
|
||||
|
||||
onSubmit() {
|
||||
// add new Replication rule
|
||||
let copyRuleForm: ReplicationRule = this.ruleForm.value;
|
||||
copyRuleForm.trigger = this.setTriggerVaule(copyRuleForm.trigger);
|
||||
if (!this.policyId) {
|
||||
this.repService.createReplicationRule(copyRuleForm)
|
||||
.then(() => {
|
||||
this.msgHandler.showSuccess('REPLICATION.CREATED_SUCCESS');
|
||||
this.inProgress = false;
|
||||
setTimeout(() => {
|
||||
this.copyUpdateForm = Object.assign({}, this.ruleForm.value);
|
||||
this.router.navigate(['/harbor/replications']);
|
||||
}, 2000);
|
||||
|
||||
}).catch((error: any) => {
|
||||
this.inProgress = false;
|
||||
this.msgHandler.handleError(error);
|
||||
});
|
||||
} else {
|
||||
this.repService.updateReplicationRule(this.policyId, this.ruleForm.value)
|
||||
.then(() => {
|
||||
this.msgHandler.showSuccess('REPLICATION.UPDATED_SUCCESS');
|
||||
this.inProgress = false;
|
||||
setTimeout(() => {
|
||||
this.copyUpdateForm = Object.assign({}, this.ruleForm.value);
|
||||
this.router.navigate(['/harbor/replications']);
|
||||
}, 2000);
|
||||
|
||||
}).catch((error: any) => {
|
||||
this.inProgress = false;
|
||||
this.msgHandler.handleError(error);
|
||||
});
|
||||
}
|
||||
this.inProgress = true;
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.router.navigate(['/harbor/replications']);
|
||||
}
|
||||
|
||||
// UTC time
|
||||
public getOfftime(daily_time: any): string {
|
||||
|
||||
let timeOffset: number = 0; // seconds
|
||||
if (daily_time && typeof daily_time === 'number') {
|
||||
timeOffset = +daily_time;
|
||||
}
|
||||
|
||||
// Convert to current time
|
||||
let timezoneOffset: number = this._localTime.getTimezoneOffset();
|
||||
// Local time
|
||||
timeOffset = timeOffset - timezoneOffset * 60;
|
||||
if (timeOffset < 0) {
|
||||
timeOffset = timeOffset + ONE_DAY_SECONDS;
|
||||
}
|
||||
|
||||
if (timeOffset >= ONE_DAY_SECONDS) {
|
||||
timeOffset -= ONE_DAY_SECONDS;
|
||||
}
|
||||
|
||||
// To time string
|
||||
let hours: number = Math.floor(timeOffset / ONE_HOUR_SECONDS);
|
||||
let minutes: number = Math.floor((timeOffset - hours * ONE_HOUR_SECONDS) / 60);
|
||||
|
||||
let timeStr: string = '' + hours;
|
||||
if (hours < 10) {
|
||||
timeStr = '0' + timeStr;
|
||||
}
|
||||
if (minutes < 10) {
|
||||
timeStr += ':0';
|
||||
} else {
|
||||
timeStr += ':';
|
||||
}
|
||||
timeStr += minutes;
|
||||
|
||||
return timeStr;
|
||||
}
|
||||
public setOfftime(v: string) {
|
||||
if (!v || v === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
let values: string[] = v.split(':');
|
||||
if (!values || values.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hours: number = +values[0];
|
||||
let minutes: number = +values[1];
|
||||
// Convert to UTC time
|
||||
let timezoneOffset: number = this._localTime.getTimezoneOffset();
|
||||
let utcTimes: number = hours * ONE_HOUR_SECONDS + minutes * 60;
|
||||
utcTimes += timezoneOffset * 60;
|
||||
if (utcTimes < 0) {
|
||||
utcTimes += ONE_DAY_SECONDS;
|
||||
}
|
||||
|
||||
if (utcTimes >= ONE_DAY_SECONDS) {
|
||||
utcTimes -= ONE_DAY_SECONDS;
|
||||
}
|
||||
|
||||
return utcTimes;
|
||||
}
|
||||
|
||||
backReplication(): void {
|
||||
this.router.navigate(['/harbor/replications']);
|
||||
}
|
||||
|
||||
getChanges(): { [key: string]: any | any[] } {
|
||||
let changes: { [key: string]: any | any[] } = {};
|
||||
let ruleValue: { [key: string]: any | any[] } = this.ruleForm.value;
|
||||
if (!ruleValue || !this.copyUpdateForm) {
|
||||
return changes;
|
||||
}
|
||||
for (let prop in ruleValue) {
|
||||
let field = this.copyUpdateForm[prop];
|
||||
if (!compareValue(field, ruleValue[prop])) {
|
||||
changes[prop] = ruleValue[prop];
|
||||
//Number
|
||||
if (typeof field === "number") {
|
||||
changes[prop] = +changes[prop];
|
||||
}
|
||||
|
||||
//Trim string value
|
||||
if (typeof field === "string") {
|
||||
changes[prop] = ('' + changes[prop]).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Created by pengf on 9/28/2017.
|
||||
*/
|
||||
|
||||
.select{
|
||||
width: 186px;
|
||||
}
|
||||
.select .optionMore{
|
||||
background-color: #bfbaba;
|
||||
height: 1.6em;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
.hideFilter{ display: none;}
|
||||
h4{
|
||||
color: #666;
|
||||
}
|
||||
label:first-child {
|
||||
font-size: 15px;
|
||||
left: -10px !important;
|
||||
}
|
||||
.endpointSelect{ width: 290px;}
|
||||
.filterSelect{width: 320px;}
|
||||
.filterSelect label{width: 160px;}
|
||||
.filterSelect label input{width: 100%;}
|
||||
.cursor{cursor: pointer;}
|
||||
.padLeft0{padding-left: 0;}
|
||||
.floatSet {display: inline-block; float: left; width: 120px;margin-right: 10px;}
|
||||
.form-group{ min-height: 36px;}
|
||||
|
||||
.projectInput{float: left;}
|
||||
.projectInput input{width: 185px;background-color: white;}
|
||||
.switchIcon{width:20px;height:20px; margin-top: 5px;margin-left: 15px;}
|
|
@ -0,0 +1,122 @@
|
|||
<div>
|
||||
<a class="cursor" (click)="backReplication()">< {{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</a>
|
||||
<h1 class="sub-header-title">{{headerTitle | translate}}</h1>
|
||||
<form [formGroup]="ruleForm" (ngSubmit)="onSubmit()" novalidate>
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label>
|
||||
<label class="col-md-8" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
|
||||
[class.invalid]='(ruleForm.controls.name.touched && ruleForm.controls.name.invalid) || isRuleNameExist'>
|
||||
<input type="text" id="ruleName" required formControlName="name" #ruleName (keyup)='checkRuleName()' autocomplete="off">
|
||||
<span class="tooltip-content">{{ruleNameTooltip | translate}}</span>
|
||||
</label><span class="spinner spinner-inline spinner-pos" [hidden]="!inNameChecking"></span>
|
||||
</div>
|
||||
<!--Description-->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
|
||||
<textarea type="text" id="ruleDescription" style=" width: 355px;" row= 3; formControlName="description"></textarea>
|
||||
</div>
|
||||
<!--Projects-->
|
||||
<h4>{{'REPLICATION.SOURCE' | translate}}</h4>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'PROJECT.PROJECTS' | translate}}<span style="color: red">*</span></label>
|
||||
<div formArrayName="projects">
|
||||
<div class="projectInput" *ngFor="let project of projects.controls; let i= index" [formGroupName]="i">
|
||||
<input formControlName="name" class="label" readonly value="name">
|
||||
</div>
|
||||
</div>
|
||||
<clr-icon shape="switch" class="is-solid switchIcon" (click)="openProjectModel()"></clr-icon>
|
||||
</div>
|
||||
|
||||
<!--images/Filter-->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'REPLICATION.FILTER' | translate}}</label>
|
||||
<div formArrayName="filters">
|
||||
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index" [formGroupName]="i">
|
||||
<div>
|
||||
<div class="select floatSet">
|
||||
<select formControlName="kind" (change)="filterChange($event)" id="{{i}}" name="{{filterListData[i]?.name}}">
|
||||
<option *ngFor="let filter of filterListData[i]?.options;" value="{{filter}}">{{filter}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
|
||||
[class.invalid]='ruleForm.controls.filters.controls[i].controls.pattern.touched && ruleForm.controls.filters.controls[i].controls.pattern.invalid'>
|
||||
<input type="text" #filterValue required size="14" formControlName="pattern">
|
||||
<span class="tooltip-content">{{'TOOLTIP.EMPTY' | translate}}</span>
|
||||
</label>
|
||||
<clr-icon shape="times-circle" class="is-solid" (click)="deleteFilter(i)"></clr-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<clr-icon shape="plus-circle" class="is-solid" [hidden]="isFilterHide" (click)="addNewFilter()" style="margin-top: 11px;"></clr-icon>
|
||||
</div>
|
||||
<!--Targets-->
|
||||
<h4>{{'REPLICATION.TARGETS' | translate}}</h4>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'DESTINATION.ENDPOINT' | translate}} <span style="color: red">*</span></label>
|
||||
<div formArrayName="targets">
|
||||
<div class="select endpointSelect" *ngFor="let target of targets.controls; let i= index" [formGroupName]="i">
|
||||
<select id="ruleTarget" (change)="targetChange($event)" formControlName="id">
|
||||
<option *ngFor="let target of targetList" value="{{target.id}}">{{target.name}}: {{target.endpoint}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Trigger-->
|
||||
<h4>{{'REPLICATION.TRIGGER' | translate}}</h4>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'REPLICATION.SCHEDULE' | translate}}</label>
|
||||
<div formGroupName="trigger">
|
||||
<!--on trigger-->
|
||||
<div class="select floatSet">
|
||||
<select id="ruleTrigger" formControlName="kind" (change)="selectTrigger($event)">
|
||||
<option *ngFor="let triggerName of triggerNames" value="{{triggerName}}">{{triggerName}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!--on push-->
|
||||
<div style="float: left;" formGroupName="schedule_param">
|
||||
<div class="select floatSet" [hidden]="!isScheduleOpt">
|
||||
<select name="scheduleType" formControlName="type" (change)="selectSchedule($event)">
|
||||
<option *ngFor="let scheduleName of scheduleNames" value="{{scheduleName}}">{{scheduleName}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!--weekly-->
|
||||
<span style="float: left;" [hidden]="!weeklySchedule || !isScheduleOpt">on </span>
|
||||
<div [hidden]="!weeklySchedule || !isScheduleOpt" class="select floatSet">
|
||||
<select name="scheduleDay" formControlName="weekday">
|
||||
<option *ngFor="let filter of weekly; let i = index" [value]="i+1">{{filter}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!--daily/time-->
|
||||
<span [hidden]="!isScheduleOpt">at </span>
|
||||
<input [hidden]="!isScheduleOpt" type="time" formControlName="offtime" required value="08:00" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 100%;" [hidden]="!isImmediate">
|
||||
<clr-checkbox [clrChecked]="false" id="ruleDeletion" formControlName="replicate_deletion">
|
||||
{{'REPLICATION.DELETE_REMOTE_IMAGES' | translate}}
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<!--Setting-->
|
||||
<h4>{{'REPLICATION.SETTING' | translate}}</h4>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'REPLICATION.SETTING' | translate}}</label>
|
||||
<div class="col-lg-7 padLeft0">
|
||||
<clr-checkbox [clrChecked]="true" id="ruleExit" formControlName="replicate_existing_image_now">
|
||||
{{'REPLICATION.REPLICATE_IMMEDIATE' | translate}}
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="offset-md-4">
|
||||
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||
<br>
|
||||
<button type="button" id="ruleBtnCancel" class="btn btn-outline" [disabled]="!hasFormChange()" (click)="onCancel()">{{ 'BUTTON.CANCEL' | translate }}</button>
|
||||
<button type="submit" id="ruleBtnOk" class="btn btn-primary" [disabled]="!ruleForm.valid || isRuleNameExist || !hasFormChange()">{{ 'BUTTON.OK' | translate }}</button>
|
||||
</div><!-- [disabled]="!ruleForm.valid"-->
|
||||
</section>
|
||||
</form>
|
||||
<list-project-model (selectedPro)="selectedProject($event)"></list-project-model>
|
||||
</div>
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Created by pengf on 12/5/2017.
|
||||
*/
|
||||
|
||||
import {Injectable} from "@angular/core";
|
||||
import {Http, RequestOptions, Headers, URLSearchParams} from "@angular/http";
|
||||
import {Observable} from "rxjs/Observable";
|
||||
import {ReplicationRule, Target} from "./replication-rule";
|
||||
import {HTTP_GET_OPTIONS, HTTP_JSON_OPTIONS} from "../../shared/shared.utils";
|
||||
import {Project} from "../../project/project";
|
||||
|
||||
@Injectable()
|
||||
export class ReplicationRuleServie {
|
||||
headers = new Headers({'Content-type': 'application/json'});
|
||||
options = new RequestOptions({'headers': this.headers});
|
||||
baseurl = '/api/policies/replication';
|
||||
targetUrl= '/api/targets';
|
||||
|
||||
constructor(private http: Http) {}
|
||||
|
||||
public createReplicationRule(replicationRule: ReplicationRule): Observable<any> | Promise<any> | any {
|
||||
/*if (!this._isValidRule(replicationRule)) {
|
||||
return Promise.reject('Bad argument');
|
||||
}*/
|
||||
|
||||
return this.http.post(this.baseurl, JSON.stringify(replicationRule), this.options).toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public getReplicationRules(projectId?: number | string, ruleName?: string): Promise<ReplicationRule[]> | ReplicationRule[] {
|
||||
let queryParams = new URLSearchParams();
|
||||
if (projectId) {
|
||||
queryParams.set('project_id', '' + projectId);
|
||||
}
|
||||
|
||||
if (ruleName) {
|
||||
queryParams.set('name', ruleName);
|
||||
}
|
||||
|
||||
return this.http.get(this.baseurl, {search: queryParams}).toPromise()
|
||||
.then(response => response.json() as ReplicationRule[])
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public getReplicationRule(policyId: number): Promise<ReplicationRule> {
|
||||
let url: string = `${this.baseurl}/${policyId}`;
|
||||
return this.http.get(url, HTTP_GET_OPTIONS).toPromise()
|
||||
.then(response => response.json() as ReplicationRule)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
|
||||
public getEndpoints(): Promise<Target[]> | Target[] {
|
||||
return this.http
|
||||
.get(this.targetUrl)
|
||||
.toPromise()
|
||||
.then(response => response.json())
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public listProjects(): Promise<Project[]> | Project[] {
|
||||
return this.http.get(`/api/projects`, HTTP_GET_OPTIONS).toPromise()
|
||||
.then(response => response.json())
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public updateReplicationRule(id: number, rep: {[key: string]: any | any[] }): Observable<any> | Promise<any> | any {
|
||||
let url: string = `${this.baseurl}/${id}`;
|
||||
return this.http.put(url, JSON.stringify(rep), HTTP_JSON_OPTIONS).toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import {Project} from "../../project/project";
|
||||
/**
|
||||
* Created by pengf on 12/7/2017.
|
||||
*/
|
||||
|
||||
export class Target {
|
||||
id: 0;
|
||||
endpoint: 'string';
|
||||
name: 'string';
|
||||
username: 'string';
|
||||
password: 'string';
|
||||
type: 0;
|
||||
insecure: true;
|
||||
creation_time: 'string';
|
||||
update_time: 'string';
|
||||
}
|
||||
|
||||
export class Filter {
|
||||
kind: string;
|
||||
pattern: string;
|
||||
constructor(kind: string, pattern: string) {
|
||||
this.kind = kind;
|
||||
this.pattern = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
export class Trigger {
|
||||
kind: string;
|
||||
schedule_param: any | {
|
||||
[key: string]: any | any[];
|
||||
};
|
||||
constructor(kind: string, param: any | { [key: string]: any | any[]; }) {
|
||||
this.kind = kind;
|
||||
this.schedule_param = param;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReplicationRule {
|
||||
id?: number;
|
||||
name: string;
|
||||
description: string;
|
||||
projects: Project[];
|
||||
targets: Target[] ;
|
||||
trigger: Trigger ;
|
||||
filters: Filter[] ;
|
||||
replicate_existing_image_now?: boolean;
|
||||
replicate_deletion?: boolean;
|
||||
}
|
||||
|
|
@ -20,22 +20,31 @@ import { TotalReplicationPageComponent } from './total-replication/total-replica
|
|||
import { DestinationPageComponent } from './destination/destination-page.component';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import {ReplicationRuleComponent} from "./replication-rule/replication-rule.component";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {ReplicationRuleServie} from "./replication-rule/replication-rule.service";
|
||||
import {ListProjectModelComponent} from "./replication-rule/list-project-model/list-project-model.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
RouterModule
|
||||
RouterModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
declarations: [
|
||||
ReplicationPageComponent,
|
||||
ReplicationManagementComponent,
|
||||
TotalReplicationPageComponent,
|
||||
DestinationPageComponent
|
||||
DestinationPageComponent,
|
||||
ReplicationRuleComponent,
|
||||
ListProjectModelComponent,
|
||||
],
|
||||
exports: [
|
||||
ReplicationPageComponent,
|
||||
DestinationPageComponent,
|
||||
TotalReplicationPageComponent
|
||||
]
|
||||
TotalReplicationPageComponent,
|
||||
ReplicationRuleComponent,
|
||||
],
|
||||
providers: [ReplicationRuleServie]
|
||||
})
|
||||
export class ReplicationModule { }
|
|
@ -1,3 +1,4 @@
|
|||
<h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</h2>
|
||||
<div style="margin-top: 24px;">
|
||||
<hbr-replication [withReplicationJob]='false' (redirect)="customRedirect($event)"></hbr-replication>
|
||||
<hbr-replication [readonly]="false" [withReplicationJob]='true' (openCreateRule)="openCreatePage()" (openEdit)="openEditPage($event)" (redirect)="customRedirect($event)"></hbr-replication>
|
||||
</div>
|
|
@ -14,7 +14,7 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
import {Router,ActivatedRoute} from "@angular/router";
|
||||
import {ReplicationRule} from "harbor-ui";
|
||||
import {ReplicationRule} from "../replication-rule/replication-rule";
|
||||
|
||||
@Component({
|
||||
selector: 'total-replication',
|
||||
|
@ -26,7 +26,15 @@ export class TotalReplicationPageComponent {
|
|||
private activeRoute: ActivatedRoute){}
|
||||
customRedirect(rule: ReplicationRule): void {
|
||||
if (rule) {
|
||||
this.router.navigate(['../../projects', rule.project_id, "replications"], { relativeTo: this.activeRoute });
|
||||
this.router.navigate(['../projects', rule.projects[0].project_id, 'replications'], { relativeTo: this.activeRoute });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openEditPage(id: number): void {
|
||||
this.router.navigate([id, 'rule'], { relativeTo: this.activeRoute });
|
||||
}
|
||||
|
||||
openCreatePage(): void {
|
||||
this.router.navigate(['new-rule'], { relativeTo: this.activeRoute });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,7 @@
|
|||
padding-right: 16px;
|
||||
margin-top: 36px;
|
||||
margin-bottom: 11px;
|
||||
}
|
||||
}
|
||||
.custom-h2 {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export class AboutDialogComponent implements OnInit{
|
|||
|
||||
ngOnInit(): void {
|
||||
// custom skin
|
||||
let customSkinObj = this.skinableConfig.getProjects();
|
||||
let customSkinObj = this.skinableConfig.getProject();
|
||||
if (customSkinObj) {
|
||||
let selectedLang = this.translate.currentLang;
|
||||
this.customName = customSkinObj;
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</ng-template>
|
||||
<ng-template [ngSwitchCase]="2">
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="isDelete">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
<button type="button" class="btn btn-danger" (click)="confirm()" [hidden]="isDelete">{{'BUTTON.DELETE' | translate}}</button>
|
||||
<button type="button" class="btn btn-danger" (click)="delete()" [hidden]="isDelete">{{'BUTTON.DELETE' | translate}}</button>
|
||||
<button type="button" class="btn btn-primary" (click)="cancel()" [disabled]="!batchOverStatus" [hidden]="!isDelete">{{'BUTTON.CLOSE' | translate}}</button>
|
||||
</ng-template>
|
||||
<ng-template [ngSwitchCase]="3">
|
||||
|
|
|
@ -112,7 +112,7 @@ export class ConfirmationDialogComponent implements OnDestroy {
|
|||
this.close();
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
delete(): void {
|
||||
if(!this.message){//Inproper condition
|
||||
this.close();
|
||||
return;
|
||||
|
@ -130,6 +130,21 @@ export class ConfirmationDialogComponent implements OnDestroy {
|
|||
data,
|
||||
target
|
||||
));
|
||||
}
|
||||
|
||||
confirm(): void {
|
||||
if(!this.message){//Inproper condition
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let data: any = this.message.data ? this.message.data : {};
|
||||
let target = this.message.targetId ? this.message.targetId : ConfirmationTargets.EMPTY;
|
||||
this.confirmationService.confirm(new ConfirmationAcknowledgement(
|
||||
ConfirmationState.CONFIRMED,
|
||||
data,
|
||||
target
|
||||
));
|
||||
this.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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.
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
CanDeactivate, Router,
|
||||
ActivatedRouteSnapshot,
|
||||
RouterStateSnapshot
|
||||
} from '@angular/router';
|
||||
|
||||
import { ConfirmationDialogService } from '../confirmation-dialog/confirmation-dialog.service';
|
||||
|
||||
import { ConfigurationComponent } from '../../config/config.component';
|
||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationState, ConfirmationTargets } from '../shared.const';
|
||||
import {ReplicationRuleComponent} from "../../replication/replication-rule/replication-rule.component";
|
||||
|
||||
@Injectable()
|
||||
export class LeavingNewRuleRouteDeactivate implements CanDeactivate<ReplicationRuleComponent> {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private confirmation: ConfirmationDialogService) { }
|
||||
|
||||
canDeactivate(
|
||||
replicateRule: ReplicationRuleComponent,
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot): Promise<boolean> | boolean {
|
||||
//Confirmation before leaving config route
|
||||
return new Promise((resolve, reject) => {
|
||||
if (replicateRule && replicateRule.hasFormChange()) {
|
||||
let msg: ConfirmationMessage = new ConfirmationMessage(
|
||||
"CONFIG.LEAVING_CONFIRMATION_TITLE",
|
||||
"CONFIG.LEAVING_CONFIRMATION_SUMMARY",
|
||||
'',
|
||||
{},
|
||||
ConfirmationTargets.CONFIG_ROUTE
|
||||
);
|
||||
this.confirmation.openComfirmDialog(msg);
|
||||
return this.confirmation.confirmationConfirm$.subscribe(msg => {
|
||||
if (msg && msg.source === ConfirmationTargets.CONFIG_ROUTE) {
|
||||
if (msg.state === ConfirmationState.CONFIRMED) {
|
||||
return resolve(true);
|
||||
} else {
|
||||
return resolve(false);//Prevent leading route
|
||||
}
|
||||
} else {
|
||||
return resolve(true);//Should go on
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ import {
|
|||
ErrorHandler,
|
||||
HarborLibraryModule
|
||||
} from 'harbor-ui';
|
||||
import {LeavingNewRuleRouteDeactivate} from "./route/leaving-new-rule-deactivate.service";
|
||||
|
||||
const uiLibConfig: IServiceConfig = {
|
||||
enablei18Support: true,
|
||||
|
@ -123,6 +124,7 @@ const uiLibConfig: IServiceConfig = {
|
|||
AuthCheckGuard,
|
||||
SignInGuard,
|
||||
LeavingConfigRouteDeactivate,
|
||||
LeavingNewRuleRouteDeactivate,
|
||||
MemberGuard,
|
||||
MessageHandlerService,
|
||||
StatisticHandler
|
||||
|
|
|
@ -12,7 +12,7 @@ export class SkinableConfig {
|
|||
constructor(private http: Http) {}
|
||||
|
||||
public getCustomFile(): Promise<any> {
|
||||
return this.http.get('../setting.json')
|
||||
return this.http.get('../static/setting.json')
|
||||
.toPromise()
|
||||
.then(response => { this.customSkinData = response.json(); return this.customSkinData; })
|
||||
.catch(error => {
|
||||
|
@ -24,9 +24,9 @@ export class SkinableConfig {
|
|||
return this.customSkinData;
|
||||
}
|
||||
|
||||
public getProjects() {
|
||||
public getProject() {
|
||||
if (this.customSkinData) {
|
||||
return this.customSkinData.projects;
|
||||
return this.customSkinData.project;
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -55,7 +55,9 @@
|
|||
"NUMBER_REQUIRED": "Field is required and should be numbers.",
|
||||
"PORT_REQUIRED": "Field is required and should be valid port number.",
|
||||
"EMAIL_EXISTING": "Email address already exists.",
|
||||
"USER_EXISTING": "Username is already in use."
|
||||
"USER_EXISTING": "Username is already in use.",
|
||||
"RULE_USER_EXISTING": "Name is already in use.",
|
||||
"EMPTY": "Name is required"
|
||||
},
|
||||
"PLACEHOLDER": {
|
||||
"CURRENT_PWD": "Enter current password",
|
||||
|
@ -100,7 +102,8 @@
|
|||
"SYSTEM_MGMT": {
|
||||
"NAME": "Administration",
|
||||
"USER": "Users",
|
||||
"REPLICATION": "Replication",
|
||||
"REGISTRY": "Registries",
|
||||
"REPLICATION": "Replications",
|
||||
"CONFIG": "Configuration"
|
||||
},
|
||||
"LOGS": "Logs"
|
||||
|
@ -301,7 +304,17 @@
|
|||
"INVALID_DATE": "Invalid date.",
|
||||
"PLACEHOLDER": "We couldn't find any replication rules!",
|
||||
"JOB_PLACEHOLDER": "We couldn't find any replication jobs!",
|
||||
"JOB_LOG_VIEWER": "View Replication Job Log"
|
||||
"JOB_LOG_VIEWER": "View Replication Job Log",
|
||||
"BACKINFO": "Please add project and endpoint first",
|
||||
"FILTER": "Filter",
|
||||
"SCHEDULE": "Schedule",
|
||||
"SETTING":"Setting",
|
||||
"TRIGGER":"Trigger",
|
||||
"TARGETS":"Target",
|
||||
"SOURCE": "Source",
|
||||
"REPLICATE": "Replicate",
|
||||
"DELETE_REMOTE_IMAGES":"Delete remote images when locally deleted",
|
||||
"REPLICATE_IMMEDIATE":"Replicate exiting images immediately"
|
||||
},
|
||||
"DESTINATION": {
|
||||
"NEW_ENDPOINT": "New Endpoint",
|
||||
|
@ -315,7 +328,7 @@
|
|||
"TEST_CONNECTION": "Test Connection",
|
||||
"TITLE_EDIT": "Edit Endpoint",
|
||||
"TITLE_ADD": "Create Endpoint",
|
||||
"DELETE": "Delete",
|
||||
"DELETE": "Delete Endpoint",
|
||||
"TESTING_CONNECTION": "Testing Connection...",
|
||||
"TEST_CONNECTION_SUCCESS": "Connection tested successfully.",
|
||||
"TEST_CONNECTION_FAILURE": "Failed to ping endpoint.",
|
||||
|
|
|
@ -51,7 +51,9 @@
|
|||
"NUMBER_REQUIRED": "El campo es obligatorio y debería ser un número.",
|
||||
"PORT_REQUIRED": "El campo es obligatorio y debería ser un número de puerto válido.",
|
||||
"EMAIL_EXISTING": "Esa dirección de email ya existe.",
|
||||
"USER_EXISTING": "Ese nombre de usuario ya existe."
|
||||
"USER_EXISTING": "Ese nombre de usuario ya existe.",
|
||||
"RULE_USER_EXISTING": "Name is already in use.",
|
||||
"EMPTY": "Name is required"
|
||||
},
|
||||
"PLACEHOLDER": {
|
||||
"CURRENT_PWD": "Introduzca la contraseña actual",
|
||||
|
@ -96,7 +98,8 @@
|
|||
"SYSTEM_MGMT": {
|
||||
"NAME": "Administración",
|
||||
"USER": "Usuarios",
|
||||
"REPLICATION": "Replicación",
|
||||
"REGISTRY": "Registries",
|
||||
"REPLICATION": "Replicacións",
|
||||
"CONFIG": "Configuración"
|
||||
},
|
||||
"LOGS": "Logs"
|
||||
|
@ -297,7 +300,16 @@
|
|||
"INVALID_DATE": "Fecha invalida.",
|
||||
"PLACEHOLDER": "We couldn't find any replication rules!",
|
||||
"JOB_PLACEHOLDER": "We couldn't find any replication jobs!",
|
||||
"JOB_LOG_VIEWER": "View Replication Job Log"
|
||||
"JOB_LOG_VIEWER": "View Replication Job Log",
|
||||
"BACKINFO": "Please add project and endpoint first",
|
||||
"FILTER": "Filter",
|
||||
"SCHEDULE": "Schedule",
|
||||
"SETTING":"Setting",
|
||||
"TRIGGER":"Trigger",
|
||||
"TARGETS":"Target",
|
||||
"SOURCE": "Source",
|
||||
"DELETE_REMOTE_IMAGES":"Delete remote images when locally deleted",
|
||||
"REPLICATE_IMMEDIATE":"Replicate exiting images immediately"
|
||||
},
|
||||
"DESTINATION": {
|
||||
"NEW_ENDPOINT": "Nuevo Endpoint",
|
||||
|
@ -311,7 +323,7 @@
|
|||
"TEST_CONNECTION": "Comprobar conexión",
|
||||
"TITLE_EDIT": "Editar Endpoint",
|
||||
"TITLE_ADD": "Crear Endpoint",
|
||||
"DELETE": "Eliminar",
|
||||
"DELETE": "Eliminar Endpoint",
|
||||
"TESTING_CONNECTION": "Comprobar conexión...",
|
||||
"TEST_CONNECTION_SUCCESS": "Conexión comprobada satisfactoriamente.",
|
||||
"TEST_CONNECTION_FAILURE": "Fallo al comprobar el endpoint.",
|
||||
|
|
|
@ -51,7 +51,9 @@
|
|||
"NUMBER_REQUIRED": "此项为必填项且为数字。",
|
||||
"PORT_REQUIRED": "此项为必填项且为合法端口号。",
|
||||
"EMAIL_EXISTING": "邮件地址已经存在。",
|
||||
"USER_EXISTING": "用户名已经存在。"
|
||||
"USER_EXISTING": "用户名已经存在。",
|
||||
"RULE_USER_EXISTING": "名称已经存在。",
|
||||
"EMPTY": "名称为必填项"
|
||||
},
|
||||
"PLACEHOLDER": {
|
||||
"CURRENT_PWD": "输入当前密码",
|
||||
|
@ -96,6 +98,7 @@
|
|||
"SYSTEM_MGMT": {
|
||||
"NAME": "系统管理",
|
||||
"USER": "用户管理",
|
||||
"REGISTRY": "仓库管理",
|
||||
"REPLICATION": "复制管理",
|
||||
"CONFIG": "配置管理"
|
||||
},
|
||||
|
@ -297,7 +300,16 @@
|
|||
"INVALID_DATE": "无效日期。",
|
||||
"PLACEHOLDER": "未发现任何复制规则!",
|
||||
"JOB_PLACEHOLDER": "未发现任何复制任务!",
|
||||
"JOB_LOG_VIEWER": "查看复制任务日志"
|
||||
"JOB_LOG_VIEWER": "查看复制任务日志",
|
||||
"BACKINFO": "请先添加项目名称和目标",
|
||||
"FILTER": "过滤",
|
||||
"SCHEDULE": "日程",
|
||||
"SETTING":"设置",
|
||||
"TRIGGER":"触发器",
|
||||
"TARGETS":"目标",
|
||||
"SOURCE": "资源",
|
||||
"DELETE_REMOTE_IMAGES":"删除本地镜像时同时也删除远程的镜像。",
|
||||
"REPLICATE_IMMEDIATE":"立即复制现有的镜像。"
|
||||
},
|
||||
"DESTINATION": {
|
||||
"NEW_ENDPOINT": "新建目标",
|
||||
|
@ -311,7 +323,7 @@
|
|||
"TEST_CONNECTION": "测试连接",
|
||||
"TITLE_EDIT": "编辑目标",
|
||||
"TITLE_ADD": "新建目标",
|
||||
"DELETE": "删除",
|
||||
"DELETE": "删除目标",
|
||||
"TESTING_CONNECTION": "正在测试连接...",
|
||||
"TEST_CONNECTION_SUCCESS": "测试连接成功。",
|
||||
"TEST_CONNECTION_FAILURE": "测试连接失败。",
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"headerBgColor": "#004a70",
|
||||
"headerBgColor": "",
|
||||
"headerLogo": "",
|
||||
"loginBgImg": "",
|
||||
"appTitle": "VMware Harbor",
|
||||
"projects": {
|
||||
"companyName": "vmware",
|
||||
"projectName": "Harbor",
|
||||
"appTitle": "",
|
||||
"project": {
|
||||
"companyName": "",
|
||||
"projectName": "",
|
||||
"introduction": {
|
||||
"zh-cn": "",
|
||||
"es-es": "",
|
||||
|
|
|
@ -16,5 +16,5 @@
|
|||
Documentation This resource provides any keywords related to the Harbor private registry appliance
|
||||
|
||||
*** Variables ***
|
||||
${administration_user_tag_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/section/ul/li[1]/a
|
||||
${administration_tag_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/section/label
|
||||
${administration_user_tag_xpath} //clr-vertical-nav-group-children/a[contains(.,'Users')]
|
||||
${administration_tag_xpath} //clr-vertical-nav-group[contains(.,'Admin')]
|
|
@ -38,14 +38,14 @@ Init LDAP
|
|||
Sleep 1
|
||||
Capture Page Screenshot
|
||||
Disable Ldap Verify Cert Checkbox
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1]
|
||||
Click Element xpath=${config_save_button_xpath}
|
||||
Sleep 2
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[3]
|
||||
Sleep 1
|
||||
Capture Page Screenshot
|
||||
|
||||
Switch To Configure
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/section/ul/li[3]/a
|
||||
Click Element xpath=${configuration_xpath}
|
||||
Sleep 2
|
||||
|
||||
Test Ldap Connection
|
||||
|
@ -89,32 +89,32 @@ Ldap Verify Cert Checkbox Should Be Disabled
|
|||
Set Pro Create Admin Only
|
||||
#set limit to admin only
|
||||
Sleep 2
|
||||
Click Element xpath=//clr-main-container//nav//ul/li[3]
|
||||
Click Element xpath=${configuration_xpath}
|
||||
Sleep 1
|
||||
Click Element xpath=//select[@id="proCreation"]
|
||||
Click Element xpath=//select[@id="proCreation"]//option[@value="adminonly"]
|
||||
Sleep 1
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1]
|
||||
Click Element xpath=${config_save_button_xpath}
|
||||
Capture Page Screenshot AdminCreateOnly.png
|
||||
|
||||
Set Pro Create Every One
|
||||
#set limit to Every One
|
||||
Click Element xpath=//clr-main-container//nav//ul/li[3]
|
||||
#set limit to Every One
|
||||
Click Element xpath=${configuration_xpath}
|
||||
Sleep 1
|
||||
Click Element xpath=//select[@id="proCreation"]
|
||||
Click Element xpath=//select[@id="proCreation"]//option[@value="everyone"]
|
||||
Sleep 1
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1]
|
||||
Click Element xpath=${config_save_button_xpath}
|
||||
Sleep 2
|
||||
Capture Page Screenshot EveryoneCreate.png
|
||||
|
||||
Disable Self Reg
|
||||
Click Element xpath=//clr-main-container//nav//ul/li[3]
|
||||
Disable Self Reg
|
||||
Click Element xpath=${configuration_xpath}
|
||||
Mouse Down xpath=${self_reg_xpath}
|
||||
Mouse Up xpath=${self_reg_xpath}
|
||||
Sleep 1
|
||||
Self Reg Should Be Disabled
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1]
|
||||
Click Element xpath=${config_save_button_xpath}
|
||||
Capture Page Screenshot DisableSelfReg.png
|
||||
Sleep 1
|
||||
|
||||
|
@ -123,7 +123,7 @@ Enable Self Reg
|
|||
Mouse Up xpath=${self_reg_xpath}
|
||||
Sleep 1
|
||||
Self Reg Should Be Enabled
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1]
|
||||
Click Element xpath=${config_save_button_xpath}
|
||||
Capture Page Screenshot EnableSelfReg.png
|
||||
Sleep 1
|
||||
|
||||
|
@ -142,13 +142,13 @@ Project Creation Should Not Display
|
|||
## System settings
|
||||
Switch To System Settings
|
||||
Sleep 1
|
||||
Click Element xpath=//clr-main-container//nav//ul/li[3]
|
||||
Click Element xpath=${configuration_xpath}
|
||||
Click Element xpath=//*[@id="config-system"]
|
||||
|
||||
Modify Token Expiration
|
||||
[Arguments] ${minutes}
|
||||
Input Text xpath=//*[@id="tokenExpiration"] ${minutes}
|
||||
Click Button xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1]
|
||||
Click Button xpath=${config_save_button_xpath}
|
||||
Sleep 1
|
||||
|
||||
Token Must Be Match
|
||||
|
@ -159,7 +159,7 @@ Token Must Be Match
|
|||
Check Verify Remote Cert
|
||||
Mouse Down xpath=//*[@id="clr-checkbox-verifyRemoteCert"]
|
||||
Mouse Up xpath=//*[@id="clr-checkbox-verifyRemoteCert"]
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1]
|
||||
Click Element xpath=${config_save_button_xpath}
|
||||
Capture Page Screenshot RemoteCert.png
|
||||
Sleep 1
|
||||
|
||||
|
@ -191,7 +191,7 @@ Config Email
|
|||
Mouse Down xpath=//*[@id="clr-checkbox-emailInsecure"]
|
||||
Mouse Up xpath=//*[@id="clr-checkbox-emailInsecure"]
|
||||
Sleep 1
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[1]
|
||||
Click Element xpath=${config_save_button_xpath}
|
||||
Sleep 6
|
||||
|
||||
Verify Email
|
||||
|
@ -206,11 +206,11 @@ Set Scan All To None
|
|||
click element //vulnerability-config//select
|
||||
click element //vulnerability-config//select/option[@value='none']
|
||||
sleep 1
|
||||
click element //config//div/button[contains(.,'SAVE')]
|
||||
click element ${config_save_button_xpath}
|
||||
Set Scan All To Daily
|
||||
click element //vulnerability-config//select
|
||||
click element //vulnerability-config//select/option[@value='daily']
|
||||
sleep 1
|
||||
click element //config//div/button[contains(.,'SAVE')]
|
||||
click element ${config_save_button_xpath}
|
||||
Click Scan Now
|
||||
click element //vulnerability-config//button[contains(.,'SCAN')]
|
|
@ -19,3 +19,5 @@ Documentation This resource provides any keywords related to the Harbor private
|
|||
${project_create_xpath} //clr-dg-action-bar//button[contains(.,'New')]
|
||||
${self_reg_xpath} //input[@id="clr-checkbox-selfReg"]
|
||||
${test_ldap_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/config/div/div/div/button[3]
|
||||
${config_save_button_xpath} //config//div/button[contains(.,'SAVE')]
|
||||
${configuration_xpath} //clr-vertical-nav-group-children/a[contains(.,'Configuration')]
|
|
@ -56,7 +56,7 @@ Switch To Log
|
|||
Sleep 1
|
||||
|
||||
Switch To Replication
|
||||
Click Element xpath=${replication_xpath}
|
||||
Click Element xpath=${project_replication_xpath}
|
||||
Sleep 1
|
||||
|
||||
Back To projects
|
||||
|
|
|
@ -20,6 +20,6 @@ ${create_project_button_css} .btn
|
|||
${project_name_xpath} //*[@id="create_project_name"]
|
||||
${project_public_xpath} //input[@name='public']/..//label
|
||||
${project_save_css} html body.no-scrolling harbor-app harbor-shell clr-main-container.main-container div.content-container div.content-area.content-area-override project div.row div.col-lg-12.col-md-12.col-sm-12.col-xs-12 div.row.flex-items-xs-between div.option-left create-project clr-modal div.modal div.modal-dialog div.modal-content div.modal-footer button.btn.btn-primary
|
||||
${log_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/a[2]
|
||||
${projects_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/a[1]
|
||||
${replication_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/nav/ul/li[4]/a
|
||||
${log_xpath} //clr-main-container/div/clr-vertical-nav/div/a[contains(.,'Logs')]
|
||||
${projects_xpath} //clr-main-container/div/clr-vertical-nav/div/a[contains(.,'Projects')]
|
||||
${project_replication_xpath} //project-detail//a[contains(.,'Replication')]
|
|
@ -22,6 +22,7 @@ ${HARBOR_VERSION} v1.1.1
|
|||
*** Keywords ***
|
||||
Create An New Rule With New Endpoint
|
||||
[Arguments] ${policy_name} ${policy_description} ${destination_name} ${destination_url} ${destination_username} ${destination_password}
|
||||
|
||||
Click element ${new_name_xpath}
|
||||
Sleep 2
|
||||
|
||||
|
@ -30,15 +31,14 @@ Create An New Rule With New Endpoint
|
|||
|
||||
#Click element xpath=${policy_enable_checkbox}
|
||||
#enable attribute is droped in new ui
|
||||
|
||||
Click element xpath=${policy_endpoint_checkbox}
|
||||
|
||||
Input text xpath=${destination_name_xpath} ${destination_name}
|
||||
Input text xpath=${destination_url_xpath} ${destination_url}
|
||||
Input text xpath=${destination_username_xpath} ${destination_username}
|
||||
Input text xpath=${destination_password_xpath} ${destination_password}
|
||||
Click element xpath=${replicaton_save_xpath}
|
||||
Click element xpath=//*[@id="ruleBtnOk"]
|
||||
Sleep 5
|
||||
Capture Page Screenshot rule_${policy_name}.png
|
||||
Wait Until Page Contains ${policy_name}
|
||||
|
||||
Wait Until Page Contains ${policy_description}
|
||||
Wait Until Page Contains ${destination_name}
|
||||
|
||||
|
|
|
@ -26,3 +26,4 @@ ${destination_url_xpath} //*[@id='destination_url']
|
|||
${destination_username_xpath} //*[@id='destination_username']
|
||||
${destination_password_xpath} //*[@id='destination_password']
|
||||
${replicaton_save_xpath} //button[contains(.,'OK')]
|
||||
${replication_xpath} //clr-vertical-nav-group-children/a[contains(.,'Replication')]
|
|
@ -31,7 +31,7 @@ Change Password
|
|||
Sleep 1
|
||||
Click Element xpath=//password-setting/clr-modal//button[2]
|
||||
Sleep 2
|
||||
Click Element xpath=/html/body/harbor-app/harbor-shell/clr-main-container/div/nav/section/a[2]
|
||||
Click Element xpath=${log_xpath}
|
||||
Sleep 1
|
||||
|
||||
Update User Comment
|
||||
|
|
|
@ -237,15 +237,15 @@ Test Case - Edit Token Expire
|
|||
Modify Token Expiration 30
|
||||
Close Browser
|
||||
|
||||
Test Case - Create An Replication Rule New Endpoint
|
||||
Init Chrome Driver
|
||||
${d}= Get current date result_format=%m%s
|
||||
Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD}
|
||||
Create An New Project project${d}
|
||||
Go Into Project project${d}
|
||||
Switch To Replication
|
||||
Create An New Rule With New Endpoint policy_name=test_policy_${d} policy_description=test_description destination_name=test_destination_name_${d} destination_url=test_destination_url_${d} destination_username=test_destination_username destination_password=test_destination_password
|
||||
Close Browser
|
||||
# Test Case - Create An Replication Rule New Endpoint
|
||||
# Init Chrome Driver
|
||||
# ${d}= Get current date result_format=%m%s
|
||||
# Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD}
|
||||
# Create An New Project project${d}
|
||||
# Go Into Project project${d}
|
||||
# Switch To Replication
|
||||
# Create An New Rule With New Endpoint policy_name=test_policy_${d} policy_description=test_description destination_name=test_destination_name_${d} destination_url=test_destination_url_${d} destination_username=test_destination_username destination_password=test_destination_password
|
||||
# Close Browser
|
||||
|
||||
Test Case - Scan A Tag In The Repo
|
||||
Init Chrome Driver
|
||||
|
|
Loading…
Reference in New Issue
Block a user