Merge remote-tracking branch 'origin/master' into fix/gitlab

This commit is contained in:
lxShaDoWxl 2020-01-02 13:43:07 +06:00
commit 13892ba144
45 changed files with 818 additions and 221 deletions

View File

@ -26,6 +26,9 @@ https:
# Remember Change the admin password from UI after launching Harbor.
harbor_admin_password: Harbor12345
#TODO: remove this temporary flag before ships v1.11/v2, this should always be true
registry_use_basic_auth: false
# Harbor DB configuration
database:
# The password for the root user of Harbor DB. Change this before any production use.

View File

@ -1,5 +1,6 @@
FROM photon:2.0
RUN tdnf install -y python3 \
&& tdnf install -y python3-pip
RUN pip3 install pipenv==2018.11.26
RUN tdnf install -y python3 python3-pip httpd
RUN pip3 install pipenv==2018.11.26

View File

@ -35,7 +35,7 @@ def main(conf, with_notary, with_clair, with_chartmuseum):
try:
validate(config_dict, notary_mode=with_notary)
except Exception as e:
logging.info('Error happend in config validation...')
logging.info('Error happened in config validation...')
logging.error(e)
sys.exit(-1)

View File

@ -44,6 +44,8 @@ RELOAD_KEY={{reload_key}}
CHART_REPOSITORY_URL={{chart_repository_url}}
REGISTRY_CONTROLLER_URL={{registry_controller_url}}
WITH_CHARTMUSEUM={{with_chartmuseum}}
REGISTRY_CREDENTIAL_USERNAME={{registry_username}}
REGISTRY_CREDENTIAL_PASSWORD={{registry_password}}
HTTP_PROXY={{core_http_proxy}}
HTTPS_PROXY={{core_https_proxy}}

View File

@ -26,11 +26,17 @@ http:
debug:
addr: localhost:5001
auth:
{% if registry_use_basic_auth %}
htpasswd:
realm: harbor-registry-basic-realm
path: /etc/registry/passwd
{% else %}
token:
issuer: harbor-token-issuer
realm: {{public_url}}/service/token
rootcertbundle: /etc/registry/root.crt
service: harbor-registry
{% endif %}
validation:
disabled: true
notifications:

View File

@ -1,14 +1,14 @@
# Get or generate private key
import os, sys, subprocess, shutil
import os, subprocess, shutil
from pathlib import Path
from subprocess import DEVNULL
from functools import wraps
from g import DEFAULT_GID, DEFAULT_UID
from .misc import (
mark_file,
generate_random_string,
check_permission)
check_permission,
stat_decorator)
SSL_CERT_PATH = os.path.join("/etc/cert", "server.crt")
SSL_CERT_KEY_PATH = os.path.join("/etc/cert", "server.key")
@ -44,19 +44,6 @@ def get_alias(path):
alias = _get_secret(path, "defaultalias", length=8)
return alias
## decorator actions
def stat_decorator(func):
@wraps(func)
def check_wrapper(*args, **kw):
stat = func(*args, **kw)
if stat == 0:
print("Generated certificate, key file: {key_path}, cert file: {cert_path}".format(**kw))
else:
print("Fail to generate key file: {key_path}, cert file: {cert_path}".format(**kw))
sys.exit(1)
return check_wrapper
@stat_decorator
def create_root_cert(subj, key_path="./k.key", cert_path="./cert.crt"):
rc = subprocess.call(["/usr/bin/openssl", "genrsa", "-out", key_path, "4096"], stdout=DEVNULL, stderr=subprocess.STDOUT)

View File

@ -9,6 +9,8 @@ default_db_max_open_conns = 0 # NOTE: https://golang.org/pkg/database/sql/#DB.S
default_https_cert_path = '/your/certificate/path'
default_https_key_path = '/your/certificate/path'
REGISTRY_USER_NAME = 'harbor_registry_user'
def validate(conf: dict, **kwargs):
# hostname validate
@ -83,6 +85,7 @@ def validate(conf: dict, **kwargs):
# TODO:
# If user enable trust cert dir, need check if the files in this dir is readable.
def parse_versions():
if not versions_file_path.is_file():
return {}
@ -90,6 +93,7 @@ def parse_versions():
versions = yaml.load(f)
return versions
def parse_yaml_config(config_file_path, with_notary, with_clair, with_chartmuseum):
'''
:param configs: config_parser object
@ -321,6 +325,12 @@ def parse_yaml_config(config_file_path, with_notary, with_clair, with_chartmuseu
# UAA configs
config_dict['uaa'] = configs.get('uaa') or {}
config_dict['registry_username'] = REGISTRY_USER_NAME
config_dict['registry_password'] = generate_random_string(32)
# TODO: remove the flag before release
config_dict['registry_use_basic_auth'] = configs['registry_use_basic_auth']
return config_dict

View File

@ -1,11 +1,10 @@
import os
import string
import os, string, sys
import secrets
from pathlib import Path
from functools import wraps
from g import DEFAULT_UID, DEFAULT_GID
# To meet security requirement
# By default it will change file mode to 0600, and make the owner of the file to 10000:10000
def mark_file(path, mode=0o600, uid=DEFAULT_UID, gid=DEFAULT_GID):
@ -154,3 +153,18 @@ def other_can_read(st_mode: int) -> bool:
Check if other user have the read permission of this st_mode
"""
return True if st_mode & 0o004 else False
# decorator actions
def stat_decorator(func):
@wraps(func)
def check_wrapper(*args, **kw):
stat = func(*args, **kw)
if stat == 0:
print("Successfully called func: %s" % func.__name__)
else:
print("Failed to call func: %s" % func.__name__)
sys.exit(1)
return check_wrapper

View File

@ -1,4 +1,4 @@
import os, copy
import os, copy, subprocess
from g import config_dir, templates_dir, DEFAULT_GID, DEFAULT_UID, data_dir
from utils.misc import prepare_dir
@ -8,6 +8,7 @@ from utils.jinja import render_jinja
registry_config_dir = os.path.join(config_dir, "registry")
registry_config_template_path = os.path.join(templates_dir, "registry", "config.yml.jinja")
registry_conf = os.path.join(config_dir, "registry", "config.yml")
registry_passwd_path = os.path.join(config_dir, "registry", "passwd")
registry_data_dir = os.path.join(data_dir, 'registry')
levels_map = {
@ -18,10 +19,13 @@ levels_map = {
'fatal': 'fatal'
}
def prepare_registry(config_dict):
prepare_dir(registry_data_dir, uid=DEFAULT_UID, gid=DEFAULT_GID)
prepare_dir(registry_config_dir)
if config_dict['registry_use_basic_auth']:
gen_passwd_file(config_dict)
storage_provider_info = get_storage_provider_info(
config_dict['storage_provider_name'],
config_dict['storage_provider_config'])
@ -55,3 +59,8 @@ def get_storage_provider_info(provider_name, provider_config):
storage_provider_conf_list.append('{}: {}'.format(config[0], value))
storage_provider_info = ('\n' + ' ' * 4).join(storage_provider_conf_list)
return storage_provider_info
def gen_passwd_file(config_dict):
return subprocess.call(["/usr/bin/htpasswd", "-bcB", registry_passwd_path, config_dict['registry_username'],
config_dict['registry_password']], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)

View File

@ -299,6 +299,11 @@ func CoreSecret() string {
return os.Getenv("CORE_SECRET")
}
// RegistryCredential returns the username and password the core uses to access registry
func RegistryCredential() (string, string) {
return os.Getenv("REGISTRY_CREDENTIAL_USERNAME"), os.Getenv("REGISTRY_CREDENTIAL_PASSWORD")
}
// JobserviceSecret returns a secret to mark Jobservice when communicate with
// other component
// TODO replace it with method of SecretStore

30
src/core/filter/orm.go Normal file
View File

@ -0,0 +1,30 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package filter
import (
"github.com/astaxie/beego/context"
o "github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/internal/orm"
)
// OrmFilter set orm.Ormer instance to the context of the http.Request
func OrmFilter(ctx *context.Context) {
if ctx == nil || ctx.Request == nil {
return
}
ctx.Request = ctx.Request.WithContext(orm.NewContext(ctx.Request.Context(), o.NewOrm()))
}

View File

@ -247,6 +247,7 @@ func main() {
filter.Init()
beego.InsertFilter("/api/*", beego.BeforeStatic, filter.SessionCheck)
beego.InsertFilter("/*", beego.BeforeRouter, filter.OrmFilter)
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
beego.InsertFilter("/*", beego.BeforeRouter, filter.ReadonlyFilter)

View File

@ -23,7 +23,6 @@ import (
"github.com/goharbor/harbor/src/core/middlewares/countquota"
"github.com/goharbor/harbor/src/core/middlewares/immutable"
"github.com/goharbor/harbor/src/core/middlewares/listrepo"
"github.com/goharbor/harbor/src/core/middlewares/multiplmanifest"
"github.com/goharbor/harbor/src/core/middlewares/readonly"
"github.com/goharbor/harbor/src/core/middlewares/regtoken"
"github.com/goharbor/harbor/src/core/middlewares/sizequota"
@ -63,17 +62,16 @@ func (b *DefaultCreator) Create() *alice.Chain {
func (b *DefaultCreator) geMiddleware(mName string) alice.Constructor {
middlewares := map[string]alice.Constructor{
CHART: func(next http.Handler) http.Handler { return chart.New(next) },
READONLY: func(next http.Handler) http.Handler { return readonly.New(next) },
URL: func(next http.Handler) http.Handler { return url.New(next) },
MUITIPLEMANIFEST: func(next http.Handler) http.Handler { return multiplmanifest.New(next) },
LISTREPO: func(next http.Handler) http.Handler { return listrepo.New(next) },
CONTENTTRUST: func(next http.Handler) http.Handler { return contenttrust.New(next) },
VULNERABLE: func(next http.Handler) http.Handler { return vulnerable.New(next) },
SIZEQUOTA: func(next http.Handler) http.Handler { return sizequota.New(next) },
COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) },
IMMUTABLE: func(next http.Handler) http.Handler { return immutable.New(next) },
REGTOKEN: func(next http.Handler) http.Handler { return regtoken.New(next) },
CHART: func(next http.Handler) http.Handler { return chart.New(next) },
READONLY: func(next http.Handler) http.Handler { return readonly.New(next) },
URL: func(next http.Handler) http.Handler { return url.New(next) },
LISTREPO: func(next http.Handler) http.Handler { return listrepo.New(next) },
CONTENTTRUST: func(next http.Handler) http.Handler { return contenttrust.New(next) },
VULNERABLE: func(next http.Handler) http.Handler { return vulnerable.New(next) },
SIZEQUOTA: func(next http.Handler) http.Handler { return sizequota.New(next) },
COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) },
IMMUTABLE: func(next http.Handler) http.Handler { return immutable.New(next) },
REGTOKEN: func(next http.Handler) http.Handler { return regtoken.New(next) },
}
return middlewares[mName]
}

View File

@ -16,24 +16,23 @@ package middlewares
// const variables
const (
CHART = "chart"
READONLY = "readonly"
URL = "url"
MUITIPLEMANIFEST = "manifest"
LISTREPO = "listrepo"
CONTENTTRUST = "contenttrust"
VULNERABLE = "vulnerable"
SIZEQUOTA = "sizequota"
COUNTQUOTA = "countquota"
IMMUTABLE = "immutable"
REGTOKEN = "regtoken"
CHART = "chart"
READONLY = "readonly"
URL = "url"
LISTREPO = "listrepo"
CONTENTTRUST = "contenttrust"
VULNERABLE = "vulnerable"
SIZEQUOTA = "sizequota"
COUNTQUOTA = "countquota"
IMMUTABLE = "immutable"
REGTOKEN = "regtoken"
)
// ChartMiddlewares middlewares for chart server
var ChartMiddlewares = []string{CHART}
// Middlewares with sequential organization
var Middlewares = []string{READONLY, URL, REGTOKEN, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
var Middlewares = []string{READONLY, URL, REGTOKEN, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
// MiddlewaresLocal ...
var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA}

View File

@ -1,48 +0,0 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package multiplmanifest
import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/middlewares/util"
"net/http"
"strings"
)
type multipleManifestHandler struct {
next http.Handler
}
// New ...
func New(next http.Handler) http.Handler {
return &multipleManifestHandler{
next: next,
}
}
// ServeHTTP The handler is responsible for blocking request to upload manifest list by docker client, which is not supported so far by Harbor.
func (mh multipleManifestHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
match, _, _ := util.MatchPushManifest(req)
if match {
contentType := req.Header.Get("Content-type")
// application/vnd.docker.distribution.manifest.list.v2+json
if strings.Contains(contentType, "manifest.list.v2") {
log.Debugf("Content-type: %s is not supported, failing the response.", contentType)
http.Error(rw, util.MarshalError("UNSUPPORTED_MEDIA_TYPE", "Manifest.list is not supported."), http.StatusUnsupportedMediaType)
return
}
}
mh.next.ServeHTTP(rw, req)
}

68
src/internal/orm/orm.go Normal file
View File

@ -0,0 +1,68 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package orm
import (
"context"
"fmt"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/utils/log"
)
type ormKey struct{}
// FromContext returns orm from context
func FromContext(ctx context.Context) (orm.Ormer, bool) {
o, ok := ctx.Value(ormKey{}).(orm.Ormer)
return o, ok
}
// NewContext returns new context with orm
func NewContext(ctx context.Context, o orm.Ormer) context.Context {
return context.WithValue(ctx, ormKey{}, o)
}
// WithTransaction a decorator which make f run in transaction
func WithTransaction(f func(ctx context.Context) error) func(ctx context.Context) error {
return func(ctx context.Context) error {
o, ok := FromContext(ctx)
if !ok {
return fmt.Errorf("ormer value not found in context")
}
tx := ormerTx{Ormer: o}
if err := tx.Begin(); err != nil {
log.Errorf("begin transaction failed: %v", err)
return err
}
if err := f(ctx); err != nil {
if e := tx.Rollback(); e != nil {
log.Errorf("rollback transaction failed: %v", e)
return e
}
return err
}
if err := tx.Commit(); err != nil {
log.Errorf("commit transaction failed: %v", err)
return err
}
return nil
}
}

View File

@ -0,0 +1,315 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package orm
import (
"context"
"errors"
"testing"
"github.com/astaxie/beego/orm"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/stretchr/testify/suite"
)
func addProject(ctx context.Context, project models.Project) (int64, error) {
o, ok := FromContext(ctx)
if !ok {
return 0, errors.New("orm not found in context")
}
return o.Insert(&project)
}
func readProject(ctx context.Context, id int64) (*models.Project, error) {
o, ok := FromContext(ctx)
if !ok {
return nil, errors.New("orm not found in context")
}
project := &models.Project{
ProjectID: id,
}
if err := o.Read(project, "project_id"); err != nil {
return nil, err
}
return project, nil
}
func deleteProject(ctx context.Context, id int64) error {
o, ok := FromContext(ctx)
if !ok {
return errors.New("orm not found in context")
}
project := &models.Project{
ProjectID: id,
}
_, err := o.Delete(project, "project_id")
return err
}
func existProject(ctx context.Context, id int64) bool {
o, ok := FromContext(ctx)
if !ok {
return false
}
project := &models.Project{
ProjectID: id,
}
if err := o.Read(project, "project_id"); err != nil {
return false
}
return true
}
// Suite ...
type OrmSuite struct {
suite.Suite
}
// SetupSuite ...
func (suite *OrmSuite) SetupSuite() {
dao.PrepareTestForPostgresSQL()
}
func (suite *OrmSuite) TestContext() {
ctx := context.TODO()
o, ok := FromContext(ctx)
suite.False(ok)
suite.Nil(o)
o, ok = FromContext(NewContext(ctx, orm.NewOrm()))
suite.True(ok)
suite.NotNil(o)
}
func (suite *OrmSuite) TestWithTransaction() {
ctx := NewContext(context.TODO(), orm.NewOrm())
var id int64
t1 := WithTransaction(func(ctx context.Context) (err error) {
id, err = addProject(ctx, models.Project{Name: "t1", OwnerID: 1})
return err
})
suite.Nil(t1(ctx))
suite.True(existProject(ctx, id))
suite.Nil(deleteProject(ctx, id))
}
func (suite *OrmSuite) TestSequentialTransactions() {
ctx := NewContext(context.TODO(), orm.NewOrm())
var id1, id2 int64
t1 := func(ctx context.Context, retErr error) error {
return WithTransaction(func(ctx context.Context) (err error) {
id1, err = addProject(ctx, models.Project{Name: "t1", OwnerID: 1})
if err != nil {
return err
}
// Ensure t1 created success
suite.True(existProject(ctx, id1))
return retErr
})(ctx)
}
t2 := func(ctx context.Context, retErr error) error {
return WithTransaction(func(ctx context.Context) (err error) {
id2, _ = addProject(ctx, models.Project{Name: "t2", OwnerID: 1})
if err != nil {
return err
}
// Ensure t2 created success
suite.True(existProject(ctx, id2))
return retErr
})(ctx)
}
if suite.Nil(t1(ctx, nil)) {
suite.True(existProject(ctx, id1))
}
if suite.Nil(t2(ctx, nil)) {
suite.True(existProject(ctx, id2))
}
// delete project t1 and t2 in db
suite.Nil(deleteProject(ctx, id1))
suite.Nil(deleteProject(ctx, id2))
if suite.Error(t1(ctx, errors.New("oops"))) {
suite.False(existProject(ctx, id1))
}
if suite.Nil(t2(ctx, nil)) {
suite.True(existProject(ctx, id2))
suite.Nil(deleteProject(ctx, id2))
}
}
func (suite *OrmSuite) TestNestedTransaction() {
ctx := NewContext(context.TODO(), orm.NewOrm())
var id1, id2 int64
nt1 := WithTransaction(func(ctx context.Context) (err error) {
id1, err = addProject(ctx, models.Project{Name: "nt1", OwnerID: 1})
return err
})
nt2 := WithTransaction(func(ctx context.Context) (err error) {
id2, err = addProject(ctx, models.Project{Name: "nt2", OwnerID: 1})
return err
})
nt := func(ctx context.Context, retErr error) error {
return WithTransaction(func(ctx context.Context) error {
if err := nt1(ctx); err != nil {
return err
}
if err := nt2(ctx); err != nil {
return err
}
// Ensure nt1 and nt2 created success
suite.True(existProject(ctx, id1))
suite.True(existProject(ctx, id2))
return retErr
})(ctx)
}
if suite.Nil(nt(ctx, nil)) {
suite.True(existProject(ctx, id1))
suite.True(existProject(ctx, id2))
// delete project nt1 and nt2 in db
suite.Nil(deleteProject(ctx, id1))
suite.Nil(deleteProject(ctx, id2))
suite.False(existProject(ctx, id1))
suite.False(existProject(ctx, id2))
}
if suite.Error(nt(ctx, errors.New("oops"))) {
suite.False(existProject(ctx, id1))
suite.False(existProject(ctx, id2))
}
// test nt1 failed but we skip it and nt2 success
suite.Nil(nt1(ctx))
suite.True(existProject(ctx, id1))
// delete nt1 here because id1 will overwrite in the following transaction
defer func(id int64) {
suite.Nil(deleteProject(ctx, id))
}(id1)
t := WithTransaction(func(ctx context.Context) error {
suite.Error(nt1(ctx))
if err := nt2(ctx); err != nil {
return err
}
// Ensure t2 created success
suite.True(existProject(ctx, id2))
return nil
})
if suite.Nil(t(ctx)) {
suite.True(existProject(ctx, id2))
// delete project t2 in db
suite.Nil(deleteProject(ctx, id2))
}
}
func (suite *OrmSuite) TestNestedSavepoint() {
ctx := NewContext(context.TODO(), orm.NewOrm())
var id1, id2 int64
ns1 := WithTransaction(func(ctx context.Context) (err error) {
id1, err = addProject(ctx, models.Project{Name: "ns1", OwnerID: 1})
return err
})
ns2 := WithTransaction(func(ctx context.Context) (err error) {
id2, err = addProject(ctx, models.Project{Name: "ns2", OwnerID: 1})
return err
})
ns := func(ctx context.Context, retErr error) error {
return WithTransaction(func(ctx context.Context) error {
if err := ns1(ctx); err != nil {
return err
}
if err := ns2(ctx); err != nil {
return err
}
// Ensure nt1 and nt2 created success
suite.True(existProject(ctx, id1))
suite.True(existProject(ctx, id2))
return retErr
})(ctx)
}
t := func(ctx context.Context, tErr, pErr error) error {
return WithTransaction(func(c context.Context) error {
ns(c, pErr)
return tErr
})(ctx)
}
// transaction commit and s1s2 commit
suite.Nil(t(ctx, nil, nil))
// Ensure nt1 and nt2 created success
suite.True(existProject(ctx, id1))
suite.True(existProject(ctx, id2))
// delete project nt1 and nt2 in db
suite.Nil(deleteProject(ctx, id1))
suite.Nil(deleteProject(ctx, id2))
suite.False(existProject(ctx, id1))
suite.False(existProject(ctx, id2))
// transaction commit and s1s2 rollback
suite.Nil(t(ctx, nil, errors.New("oops")))
// Ensure nt1 and nt2 created failed
suite.False(existProject(ctx, id1))
suite.False(existProject(ctx, id2))
// transaction rollback and s1s2 commit
suite.Error(t(ctx, errors.New("oops"), nil))
// Ensure nt1 and nt2 created failed
suite.False(existProject(ctx, id1))
suite.False(existProject(ctx, id2))
}
func TestRunOrmSuite(t *testing.T) {
suite.Run(t, new(OrmSuite))
}

77
src/internal/orm/tx.go Normal file
View File

@ -0,0 +1,77 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package orm
import (
"encoding/hex"
"fmt"
"github.com/astaxie/beego/orm"
"github.com/google/uuid"
)
// ormerTx transaction which support savepoint
type ormerTx struct {
orm.Ormer
savepoint string
}
func (o *ormerTx) savepointMode() bool {
return o.savepoint != ""
}
func (o *ormerTx) createSavepoint() error {
val := uuid.New()
o.savepoint = fmt.Sprintf("p%s", hex.EncodeToString(val[:]))
_, err := o.Raw(fmt.Sprintf("SAVEPOINT %s", o.savepoint)).Exec()
return err
}
func (o *ormerTx) releaseSavepoint() error {
_, err := o.Raw(fmt.Sprintf("RELEASE SAVEPOINT %s", o.savepoint)).Exec()
return err
}
func (o *ormerTx) rollbackToSavepoint() error {
_, err := o.Raw(fmt.Sprintf("ROLLBACK TO SAVEPOINT %s", o.savepoint)).Exec()
return err
}
func (o *ormerTx) Begin() error {
err := o.Ormer.Begin()
if err == orm.ErrTxHasBegan {
// transaction has began for the ormer, so begin nested transaction by savepoint
return o.createSavepoint()
}
return err
}
func (o *ormerTx) Commit() error {
if o.savepointMode() {
return o.releaseSavepoint()
}
return o.Ormer.Commit()
}
func (o *ormerTx) Rollback() error {
if o.savepointMode() {
return o.rollbackToSavepoint()
}
return o.Ormer.Rollback()
}

View File

@ -62,7 +62,7 @@
{{'CONFIG.LABEL' | translate }}
</a>
<a clrVerticalNavLink routerLink="/harbor/project-quotas" routerLinkActive="active">
<clr-icon shape="volume" clrVerticalNavIcon></clr-icon>
<clr-icon shape="resource-pool" clrVerticalNavIcon></clr-icon>
{{'CONFIG.PROJECT_QUOTAS' | translate }}
</a>
<a clrVerticalNavLink routerLink="/harbor/interrogation-services" routerLinkActive="active">
@ -95,4 +95,4 @@
<account-settings-modal></account-settings-modal>
<password-setting></password-setting>
<confiramtion-dialog></confiramtion-dialog>
<about-dialog></about-dialog>
<about-dialog></about-dialog>

View File

@ -105,4 +105,4 @@
<button type="button" class="btn btn-primary" [disabled]="!isValid" (click)="onSave()">{{'BUTTON.SAVE' | translate}}</button>
</div>
</clr-modal>
</clr-modal>

View File

@ -6,7 +6,7 @@
<clr-tabs id="project-tabs" class="tabs" [class.in-overflow]="isTabLinkInOverFlow()">
<ng-container *ngFor="let tab of tabLinkNavList;let i=index">
<ng-container *ngIf="tab.permissions()">
<clr-tab >
<clr-tab>
<button [class.clear-default-active]="isDefaultTab(tab, i)" [clrTabLinkInOverflow]="tab.tabLinkInOverflow" id="{{'project-'+tab.linkName}}" clrTabLink
routerLink="{{tab.linkName}}" routerLinkActive="active" type="button">
<a class="nav-link">{{tab.showTabName | translate}}</a></button>

View File

@ -42,16 +42,3 @@ button {
padding: 0;
}
}
.in-overflow {
::ng-deep {
.tabs-overflow {
> .nav-item {
> button {
box-shadow: 0 -3px 0 #0077b8 inset;
color: 0077b8;
}
}
}
}
}

View File

@ -11,21 +11,24 @@
// 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, OnInit } from '@angular/core';
import { Component, OnInit, HostListener, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Project } from '../project';
import { SessionService } from '../../shared/session.service';
import { AppConfigService } from "../../app-config.service";
import { forkJoin } from "rxjs";
import { ProjectService, UserPermissionService, USERSTATICPERMISSION } from "../../../lib/services";
import { forkJoin, Subject, Subscription } from "rxjs";
import { UserPermissionService, USERSTATICPERMISSION } from "../../../lib/services";
import { ErrorHandler } from "../../../lib/utils/error-handler";
import { debounceTime } from 'rxjs/operators';
import { DOWN, SHOW_ELLIPSIS_WIDTH, UP } from './project-detail.const';
@Component({
selector: 'project-detail',
templateUrl: 'project-detail.component.html',
styleUrls: ['project-detail.component.scss']
})
export class ProjectDetailComponent implements OnInit {
export class ProjectDetailComponent implements OnInit, AfterViewInit, OnDestroy {
hasSignedIn: boolean;
currentProject: Project;
@ -89,29 +92,32 @@ export class ProjectDetailComponent implements OnInit {
},
{
linkName: "tag-strategy",
tabLinkInOverflow: true,
tabLinkInOverflow: false,
showTabName: "PROJECT_DETAIL.TAG_STRATEGY",
permissions: () => this.hasTagRetentionPermission
},
{
linkName: "robot-account",
tabLinkInOverflow: true,
tabLinkInOverflow: false,
showTabName: "PROJECT_DETAIL.ROBOT_ACCOUNTS",
permissions: () => this.hasRobotListPermission
},
{
linkName: "webhook",
tabLinkInOverflow: true,
tabLinkInOverflow: false,
showTabName: "PROJECT_DETAIL.WEBHOOKS",
permissions: () => this.hasWebhookListPermission
},
{
linkName: "logs",
tabLinkInOverflow: true,
tabLinkInOverflow: false,
showTabName: "PROJECT_DETAIL.LOGS",
permissions: () => this.hasLogListPermission
}
];
previousWindowWidth: number;
private _subject = new Subject<string>();
private _subscription: Subscription;
constructor(
private route: ActivatedRoute,
private router: Router,
@ -119,8 +125,7 @@ export class ProjectDetailComponent implements OnInit {
private appConfigService: AppConfigService,
private userPermissionService: UserPermissionService,
private errorHandler: ErrorHandler,
private projectService: ProjectService) {
private cdf: ChangeDetectorRef) {
this.hasSignedIn = this.sessionService.getCurrentUser() !== null;
this.route.data.subscribe(data => {
this.currentProject = <Project>data['projectResolver'];
@ -131,6 +136,32 @@ export class ProjectDetailComponent implements OnInit {
ngOnInit() {
this.projectId = this.route.snapshot.params['id'];
this.getPermissionsList(this.projectId);
if (!this._subscription) {
this._subscription = this._subject.pipe(
debounceTime(100)
).subscribe(
type => {
if (type === DOWN) {
this.resetTabsForDownSize();
} else {
this.resetTabsForUpSize();
}
}
);
}
}
ngAfterViewInit() {
this.previousWindowWidth = window.innerWidth;
setTimeout(() => {
this.resetTabsForDownSize();
}, 0);
}
ngOnDestroy() {
if (this._subscription) {
this._subscription.unsubscribe();
this._subscription = null;
}
}
getPermissionsList(projectId: number): void {
let permissionsList = [];
@ -153,17 +184,17 @@ export class ProjectDetailComponent implements OnInit {
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.LABEL.KEY, USERSTATICPERMISSION.LABEL.VALUE.CREATE));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.TAG_RETENTION.KEY, USERSTATICPERMISSION.TAG_RETENTION.VALUE.READ));
USERSTATICPERMISSION.TAG_RETENTION.KEY, USERSTATICPERMISSION.TAG_RETENTION.VALUE.READ));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.WEBHOOK.KEY, USERSTATICPERMISSION.WEBHOOK.VALUE.LIST));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.SCANNER.KEY, USERSTATICPERMISSION.SCANNER.VALUE.READ));
USERSTATICPERMISSION.SCANNER.KEY, USERSTATICPERMISSION.SCANNER.VALUE.READ));
forkJoin(...permissionsList).subscribe(Rules => {
[this.hasProjectReadPermission, this.hasLogListPermission, this.hasConfigurationListPermission, this.hasMemberListPermission
, this.hasLabelListPermission, this.hasRepositoryListPermission, this.hasHelmChartsListPermission, this.hasRobotListPermission
, this.hasLabelCreatePermission, this.hasTagRetentionPermission, this.hasWebhookListPermission,
this.hasScannerReadPermission] = Rules;
this.hasScannerReadPermission] = Rules;
}, error => this.errorHandler.error(error));
}
@ -188,10 +219,46 @@ export class ProjectDetailComponent implements OnInit {
isDefaultTab(tab, index) {
return this.route.snapshot.children[0].routeConfig.path !== tab.linkName && index === 0;
}
isTabLinkInOverFlow() {
return this.tabLinkNavList.some(tab => {
return tab.tabLinkInOverflow && this.route.snapshot.children[0].routeConfig.path === tab.linkName;
});
}
@HostListener('window:resize', ['$event'])
onResize(event) {
if (this.previousWindowWidth) {
// down size
if (this.previousWindowWidth > event.target.innerWidth) {
this._subject.next(DOWN);
} else { // up size
this._subject.next(UP);
}
}
this.previousWindowWidth = event.target.innerWidth;
}
resetTabsForDownSize(): void {
this.tabLinkNavList.filter(item => !item.tabLinkInOverflow).forEach((item, index) => {
const tabEle: HTMLElement = document.getElementById('project-' + item.linkName);
// strengthen code
if (tabEle && tabEle.getBoundingClientRect) {
const right: number = window.innerWidth - document.getElementById('project-' + item.linkName).getBoundingClientRect().right;
if (right < SHOW_ELLIPSIS_WIDTH) {
this.tabLinkNavList[index].tabLinkInOverflow = true;
}
}
});
}
resetTabsForUpSize() {
// 1.Set tabLinkInOverflow to false for all tabs(show all tabs)
for ( let i = 0; i < this.tabLinkNavList.length; i++) {
this.tabLinkNavList[i].tabLinkInOverflow = false;
}
// 2.Manually detect changes to rerender dom
this.cdf.detectChanges();
// 3. Hide overflowed tabs
this.resetTabsForDownSize();
}
}

View File

@ -0,0 +1,3 @@
export const SHOW_ELLIPSIS_WIDTH = 80;
export const DOWN: string = "down";
export const UP: string = "up";

View File

@ -0,0 +1,11 @@
// styles for dark and light theme should be defined here.
.in-overflow {
.tabs-overflow {
> .nav-item {
> button {
box-shadow: 0 -3px 0 $dark-active-tab-color inset;
color: 0077b8;
}
}
}
}

View File

@ -1,7 +1,9 @@
// Variables for dark theme should be defined here.
@import "../../node_modules/@clr/ui/clr-ui-dark.min.css";
$dark-background-color: rgb(27, 42, 50) !important;
$dark-font-color1: #acbac3 !important;
$dark-font-color-title1: #eaedf0 !important;
$dark-active-tab-color: #4aaed9;
.label-form {
background-color: #212129 !important;
@ -110,4 +112,6 @@ clr-dg-action-overflow {
color: #1b2a32;
}
}
}
}
@import "./common.scss";

View File

@ -1 +1,5 @@
@import "../../node_modules/@clr/ui/clr-ui.min.css";
// Variables for dark theme should be defined here.
@import "../../node_modules/@clr/ui/clr-ui.min.css";
$dark-active-tab-color: #0077b8;
@import "./common.scss";

View File

@ -75,7 +75,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="onCancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" [disabled]="!isValid"
<button id="edit-quota-save" type="button" class="btn btn-primary" [disabled]="!isValid"
(click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>
</clr-modal>

View File

@ -1,10 +1,9 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { EditProjectQuotasComponent } from './edit-project-quotas.component';
import { SharedModule } from '../../../../utils/shared/shared.module';
import { InlineAlertComponent } from '../../../inline-alert/inline-alert.component';
import { SERVICE_CONFIG, IServiceConfig } from '../../../../entities/service.config';
import { RouterModule } from '@angular/router';
import { EditQuotaQuotaInterface } from '../../../../services';
import { HarborLibraryModule } from '../../../../harbor-library.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
describe('EditProjectQuotasComponent', () => {
let component: EditProjectQuotasComponent;
@ -12,13 +11,20 @@ describe('EditProjectQuotasComponent', () => {
let config: IServiceConfig = {
quotaUrl: "/api/quotas/testing"
};
const mockedEditQuota: EditQuotaQuotaInterface = {
editQuota: "Edit Default Project Quotas",
setQuota: "Set the default project quotas when creating new projects",
countQuota: "Default artifact count",
storageQuota: "Default storage consumption",
quotaHardLimitValue: {storageLimit: -1, storageUnit: "Byte", countLimit: -1},
isSystemDefaultQuota: true
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
RouterModule.forRoot([])
HarborLibraryModule,
BrowserAnimationsModule
],
declarations: [ EditProjectQuotasComponent, InlineAlertComponent ],
providers: [
{ provide: SERVICE_CONFIG, useValue: config },
]
@ -34,4 +40,18 @@ describe('EditProjectQuotasComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should open', async () => {
component.openEditQuota = true;
fixture.detectChanges();
await fixture.whenStable();
component.openEditQuotaModal(mockedEditQuota);
fixture.detectChanges();
await fixture.whenStable();
let countInput: HTMLInputElement = fixture.nativeElement.querySelector('#count');
countInput.value = "100";
countInput.dispatchEvent(new Event("input"));
fixture.detectChanges();
await fixture.whenStable();
expect(component.isValid).toBeTruthy();
});
});

View File

@ -6,7 +6,7 @@
<div class="default-quota-text">
<span class="width-10rem">{{'QUOTA.PROJECT_QUOTA_DEFAULT_ARTIFACT' | translate}}</span>
<span class="num-count">{{ quotaHardLimitValue?.countLimit === -1 ? ('QUOTA.UNLIMITED'| translate) : quotaHardLimitValue?.countLimit }}</span>
<button class="btn btn-link btn-sm default-quota-edit-button"
<button id="open-edit" class="btn btn-link btn-sm default-quota-edit-button"
(click)="editDefaultQuota(quotaHardLimitValue)">{{'QUOTA.EDIT' | translate}}</button>
</div>
<div class="default-quota-text">

View File

@ -1,11 +1,7 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';
import { ProjectQuotasComponent } from './project-quotas.component';
import { IServiceConfig, SERVICE_CONFIG } from '../../../entities/service.config';
import { SharedModule } from '../../../utils/shared/shared.module';
import { RouterModule } from '@angular/router';
import { EditProjectQuotasComponent } from './edit-project-quotas/edit-project-quotas.component';
import { InlineAlertComponent } from '../../inline-alert/inline-alert.component';
import { Router } from '@angular/router';
import {
ConfigurationService, ConfigurationDefaultService, QuotaService
, QuotaDefaultService, Quota, RequestQueryParams
@ -14,8 +10,12 @@ import { ErrorHandler } from '../../../utils/error-handler';
import { of } from 'rxjs';
import { delay } from 'rxjs/operators';
import {APP_BASE_HREF} from '@angular/common';
import { HarborLibraryModule } from '../../../harbor-library.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
describe('ProjectQuotasComponent', () => {
let spy: jasmine.Spy;
let spyUpdate: jasmine.Spy;
let spyRoute: jasmine.Spy;
let quotaService: QuotaService;
let component: ProjectQuotasComponent;
@ -43,20 +43,35 @@ describe('ProjectQuotasComponent', () => {
},
}
];
const fakedRouter = {
navigate() {
return undefined;
}
};
const fakedErrorHandler = {
error() {
return undefined;
},
info() {
return undefined;
}
};
const timeout = (ms: number) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
RouterModule.forRoot([])
HarborLibraryModule,
BrowserAnimationsModule
],
declarations: [ProjectQuotasComponent, EditProjectQuotasComponent, InlineAlertComponent],
providers: [
ErrorHandler,
{ provide: ErrorHandler, useValue: fakedErrorHandler },
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: ConfigurationService, useClass: ConfigurationDefaultService },
{ provide: QuotaService, useClass: QuotaDefaultService },
{ provide: APP_BASE_HREF, useValue : '/' }
{ provide: APP_BASE_HREF, useValue : '/' },
{ provide: Router, useValue: fakedRouter }
]
})
.compileComponents();
@ -83,11 +98,52 @@ describe('ProjectQuotasComponent', () => {
};
return of(httpRes).pipe(delay(0));
});
spyUpdate = spyOn(quotaService, 'updateQuota').and.returnValue(of(null));
spyRoute = spyOn(fixture.debugElement.injector.get(Router), 'navigate').and.returnValue(of(null));
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should open edit quota modal', async () => {
// wait getting list and rendering
await timeout(10);
fixture.detectChanges();
await fixture.whenStable();
const openEditButton: HTMLButtonElement = fixture.nativeElement.querySelector("#open-edit");
openEditButton.dispatchEvent(new Event("click"));
fixture.detectChanges();
await fixture.whenStable();
const modal: HTMLElement = fixture.nativeElement.querySelector("clr-modal");
expect(modal).toBeTruthy();
});
it('edit quota', async () => {
// wait getting list and rendering
await timeout(10);
fixture.detectChanges();
await fixture.whenStable();
component.editQuota(component.quotaList[0]);
fixture.detectChanges();
await fixture.whenStable();
const countInput: HTMLInputElement = fixture.nativeElement.querySelector('#count');
countInput.value = "100";
countInput.dispatchEvent(new Event("input"));
fixture.detectChanges();
await fixture.whenStable();
const saveButton: HTMLInputElement = fixture.nativeElement.querySelector('#edit-quota-save');
saveButton.dispatchEvent(new Event("click"));
fixture.detectChanges();
await fixture.whenStable();
expect(spyUpdate.calls.count()).toEqual(1);
});
it('should call navigate function', async () => {
// wait getting list and rendering
await timeout(10);
fixture.detectChanges();
await fixture.whenStable();
const a: HTMLElement = fixture.nativeElement.querySelector('clr-dg-cell a');
a.dispatchEvent(new Event("click"));
expect(spyRoute.calls.count()).toEqual(1);
});
});

View File

@ -16,7 +16,6 @@ package image
import (
"errors"
"fmt"
"strings"
"github.com/docker/distribution/manifest/manifestlist"
@ -259,6 +258,7 @@ func (t *transfer) pullManifest(repository, reference string) (
return nil, "", nil
}
t.logger.Infof("pulling the manifest of image %s:%s ...", repository, reference)
// TODO add OCI media types
manifest, digest, err := t.src.PullManifest(repository, reference, []string{
schema1.MediaTypeManifest,
schema1.MediaTypeSignedManifest,
@ -271,47 +271,7 @@ func (t *transfer) pullManifest(repository, reference string) (
}
t.logger.Infof("the manifest of image %s:%s pulled", repository, reference)
// this is a solution to work around that harbor doesn't support manifest list
return t.handleManifest(manifest, repository, digest)
}
// if the media type of the specified manifest is manifest list, just abstract one
// manifest from the list and return it
func (t *transfer) handleManifest(manifest distribution.Manifest, repository, digest string) (
distribution.Manifest, string, error) {
mediaType, _, err := manifest.Payload()
if err != nil {
t.logger.Errorf("failed to call the payload method for manifest of %s:%s: %v", repository, digest, err)
return nil, "", err
}
// manifest
if mediaType == schema1.MediaTypeManifest ||
mediaType == schema1.MediaTypeSignedManifest ||
mediaType == schema2.MediaTypeManifest {
return manifest, digest, nil
}
// manifest list
t.logger.Info("trying abstract a manifest from the manifest list...")
manifestlist, ok := manifest.(*manifestlist.DeserializedManifestList)
if !ok {
err := fmt.Errorf("the object isn't a DeserializedManifestList")
t.logger.Errorf(err.Error())
return nil, "", err
}
digest = ""
for _, reference := range manifestlist.Manifests {
if strings.ToLower(reference.Platform.Architecture) == "amd64" &&
strings.ToLower(reference.Platform.OS) == "linux" {
digest = reference.Digest.String()
t.logger.Infof("a manifest(architecture: amd64, os: linux) found, using this one: %s", digest)
break
}
}
if len(digest) == 0 {
digest = manifest.References()[0].Digest.String()
t.logger.Infof("no manifest(architecture: amd64, os: linux) found, using the first one: %s", digest)
}
return t.pullManifest(repository, digest)
return manifest, digest, nil
}
func (t *transfer) exist(repository, tag string) (bool, string, error) {

View File

@ -21,28 +21,28 @@ Resource ../../resources/Util.robot
*** Keywords ***
Assign User Admin
[Arguments] ${user}
Click Element xpath=//harbor-user//hbr-filter//clr-icon
Retry Element Click xpath=//harbor-user//hbr-filter//clr-icon
Input Text xpath=//harbor-user//hbr-filter//input ${user}
Sleep 2
#select checkbox
Click Element //clr-dg-row[contains(.,'${user}')]//label
Retry Element Click //clr-dg-row[contains(.,'${user}')]//label
#click assign admin
Click Element //*[@id='set-admin']
Retry Element Click //*[@id='set-admin']
Sleep 1
Switch to User Tag
Click Element xpath=${administration_user_tag_xpath}
Retry Element Click xpath=${administration_user_tag_xpath}
Sleep 1
Administration Tag Should Display
Page Should Contain Element xpath=${administration_tag_xpath}
Retry Wait Until Page Contains Element xpath=${administration_tag_xpath}
User Email Should Exist
[Arguments] ${email}
Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD}
Switch to User Tag
Page Should Contain Element xpath=//clr-dg-cell[contains(., '${email}')]
Retry Wait Until Page Contains Element xpath=//clr-dg-cell[contains(., '${email}')]
Add User Button Should Be Disabled
Sleep 1
Page Should Contain Element //button[contains(.,'New') and @disabled='']
Retry Wait Until Page Contains Element //button[contains(.,'New') and @disabled='']

View File

@ -140,10 +140,10 @@ Self Reg Should Be Enabled
Checkbox Should Be Selected xpath=${self_reg_xpath}
Project Creation Should Display
Page Should Contain Element xpath=${project_create_xpath}
Retry Wait Until Page Contains Element xpath=${project_create_xpath}
Project Creation Should Not Display
Page Should Not Contain Element xpath=${project_create_xpath}
Retry Wait Until Page Not Contains Element xpath=${project_create_xpath}
## System settings
Switch To System Settings
@ -153,6 +153,8 @@ Switch To System Settings
Sleep 1
Switch To Project Quotas
Sleep 1
Retry Element Click xpath=${configuration_xpath}
Sleep 1
Retry Element Click xpath=//clr-main-container//clr-vertical-nav//a[contains(.,'Project Quotas')]
Sleep 1

View File

@ -37,7 +37,7 @@ Capture Screenshot And Source
Log Source
Sign Up Should Not Display
Page Should Not Contain Element xpath=${sign_up_button_xpath}
Retry Wait Until Page Not Contains Element xpath=${sign_up_button_xpath}
Create An New User
[Arguments] ${url} ${username} ${email} ${realname} ${newPassword} ${comment}

View File

@ -21,8 +21,8 @@ Resource ../../resources/Util.robot
*** Keywords ***
Ldap User Should Not See Change Password
Click Element //clr-header//clr-dropdown[2]//button
Sleep 1
Retry Element Click //clr-header//clr-dropdown[2]//button
Sleep 2
Page Should Not Contain Password

View File

@ -42,7 +42,7 @@ Public Should Be Selected
Project Should Be Public
[Arguments] ${projectName}
Page Should Contain Element //clr-dg-row[contains(.,'${projectName}')]//clr-dg-cell[contains(.,'Public')]
Retry Wait Until Page Contains Element //clr-dg-row[contains(.,'${projectName}')]//clr-dg-cell[contains(.,'Public')]
Content Trust Should Be Selected
Checkbox Should Be Selected //hbr-project-policy-config//input[@name='content-trust']

View File

@ -70,13 +70,13 @@ User Can Change Role
[arguments] ${username}
Retry Element Click xpath=//clr-dg-row[contains(.,'${username}')]//input/../label
Retry Element Click xpath=//*[@id='member-action']
Page Should Not Contain Element xpath=//button[@disabled='' and contains(.,'Admin')]
Retry Wait Until Page Not Contains Element xpath=//button[@disabled='' and contains(.,'Admin')]
User Can Not Change Role
[arguments] ${username}
Retry Element Click xpath=//clr-dg-row[contains(.,'${username}')]//input/../label
Retry Element Click xpath=//*[@id='member-action']
Page Should Contain Element xpath=//button[@disabled='' and contains(.,'Admin')]
Retry Wait Until Page Contains Element xpath=//button[@disabled='' and contains(.,'Admin')]
#this keyworkd seems will not use any more, will delete in the future
Non-admin View Member Account
@ -84,7 +84,7 @@ Non-admin View Member Account
Xpath Should Match X Times //clr-dg-row-master ${times}
User Can Not Add Member
Page Should Contain Element xpath=//button[@disabled='' and contains(.,'User')]
Retry Wait Until Page Contains Element xpath=//button[@disabled='' and contains(.,'User')]
Add Guest Member To Project
[arguments] ${member}
@ -153,7 +153,7 @@ User Should Be Guest
Go Into Project ${project}
Switch To Member
User Can Not Add Member
Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Guest')]
Retry Wait Until Page Contains Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Guest')]
Logout Harbor
Pull image ${ip} ${user} ${password} ${project} hello-world
Cannot Push image ${ip} ${user} ${password} ${project} hello-world
@ -168,7 +168,7 @@ User Should Be Developer
Go Into Project ${project}
Switch To Member
User Can Not Add Member
Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Developer')]
Retry Wait Until Page Contains Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Developer')]
Logout Harbor
Push Image With Tag ${ip} ${user} ${password} ${project} hello-world v1
@ -183,7 +183,7 @@ User Should Be Admin
Switch To Member
Add Guest Member To Project ${guest}
User Can Change Role ${guest}
Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Admin')]
Retry Wait Until Page Contains Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Admin')]
Logout Harbor
Push Image With Tag ${ip} ${user} ${password} ${project} hello-world v2
@ -197,7 +197,7 @@ User Should Be Master
Go Into Project ${project}
Delete Repo ${project}
Switch To Member
Page Should Contain Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Master')]
Retry Wait Until Page Contains Element xpath=//clr-dg-row[contains(.,'${user}')]//clr-dg-cell[contains(.,'Master')]
Logout Harbor
Push Image With Tag ${ip} ${user} ${password} ${project} hello-world v3
@ -206,5 +206,5 @@ Project Should Have Member
Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD}
Go Into Project ${project}
Switch To Member
Page Should Contain Element xpath=//clr-dg-cell[contains(., '${user}')]
Retry Wait Until Page Contains Element xpath=//clr-dg-cell[contains(., '${user}')]
Logout Harbor

View File

@ -21,3 +21,6 @@ ${first_cve_xpath} //clr-dg-row[1]//clr-dg-cell//a
${view_log_xpath} //clr-dg-row[1]//clr-dg-cell[4]//a
${build_history_btn} //button[contains(.,'Build History')]
${build_history_data} //clr-dg-row
${push_image_command_btn} //hbr-push-image-button//button

View File

@ -27,7 +27,7 @@ Create An New Project
Capture Page Screenshot
Retry Text Input xpath=${project_name_xpath} ${projectname}
${element_project_public}= Set Variable xpath=${project_public_xpath}
Run Keyword If '${public}' == 'true' Run Keywords Wait Until Element Is Visible And Enabled ${element_project_public} AND Click Element ${element_project_public}
Run Keyword If '${public}' == 'true' Run Keywords Wait Until Element Is Visible And Enabled ${element_project_public} AND Retry Element Click ${element_project_public}
Run Keyword If '${count_quota}'!='${null}' Input Count Quota ${count_quota}
Run Keyword If '${storage_quota}'!='${null}' Input Storage Quota ${storage_quota} ${storage_quota_unit}
Capture Page Screenshot

View File

@ -43,4 +43,4 @@ Logout Harbor
Retry Link Click Log Out
Capture Page Screenshot Logout.png
Sleep 2
Wait Until Keyword Succeeds 5x 1 Page Should Contain Element ${sign_in_title_xpath}
Wait Until Keyword Succeeds 5x 1 Retry Wait Until Page Contains Element ${sign_in_title_xpath}

View File

@ -406,7 +406,7 @@ Test Case - Developer Operate Labels
Sign In Harbor ${HARBOR_URL} user022 Test1@34
Go Into Project project${d} has_image=${false}
Sleep 3
Page Should Not Contain Element xpath=//a[contains(.,'Labels')]
Retry Wait Until Page Not Contains Element xpath=//a[contains(.,'Labels')]
Close Browser
Test Case - Retag A Image Tag
@ -431,7 +431,7 @@ Test Case - Retag A Image Tag
Page Should Contain ${target_image_name}
Go Into Repo project${random_num1}${random_num2}/${target_image_name}
Sleep 1
Page Should Contain Element xpath=${tag_value_xpath}
Retry Wait Until Page Contains Element xpath=${tag_value_xpath}
Close Browser
Test Case - Create An New Project With Quotas Set

View File

@ -44,6 +44,7 @@ Test Case - System Admin On-board New Member
${d}= Get Current Date result_format=%m%s
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Switch To User Tag
Sleep 2
Page Should Not Contain mike02
Navigate To Projects
Create An New Project project${d}
@ -60,8 +61,10 @@ Test Case - LDAP User On-borad New Member
Create An New Project project${d}
Go Into Project project${d} has_image=${false}
Switch To Member
Sleep 2
Page Should Not Contain mike04
Add Guest Member To Project mike04
Sleep 2
Page Should Contain mike04
Close Browser

View File

@ -393,7 +393,7 @@ TestCase - Developer Operate Labels
Sign In Harbor ${HARBOR_URL} bob${d} Test1@34
Go Into Project project${d} has_image=${false}
Sleep 3
Page Should Not Contain Element xpath=//a[contains(.,'Labels')]
Retry Wait Until Page Not Contains Element xpath=//a[contains(.,'Labels')]
Close Browser
Test Case - Scan A Tag In The Repo

View File

@ -36,7 +36,7 @@ Test Case - Home Page Differences With DB Mode
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Logout Harbor
Sleep 2
Page Should Not Contain Sign up
Page Should Not Contain Sign up
Page Should Not Contain Forgot password
Close Browser