mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-02 22:18:29 +01:00
Merge remote-tracking branch 'origin/master' into fix/gitlab
This commit is contained in:
commit
13892ba144
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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}}
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
30
src/core/filter/orm.go
Normal 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()))
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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
68
src/internal/orm/orm.go
Normal 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
|
||||
}
|
||||
}
|
315
src/internal/orm/orm_test.go
Normal file
315
src/internal/orm/orm_test.go
Normal 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
77
src/internal/orm/tx.go
Normal 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()
|
||||
}
|
@ -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>
|
||||
|
@ -105,4 +105,4 @@
|
||||
<button type="button" class="btn btn-primary" [disabled]="!isValid" (click)="onSave()">{{'BUTTON.SAVE' | translate}}</button>
|
||||
</div>
|
||||
|
||||
</clr-modal>
|
||||
</clr-modal>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
export const SHOW_ELLIPSIS_WIDTH = 80;
|
||||
export const DOWN: string = "down";
|
||||
export const UP: string = "up";
|
11
src/portal/src/css/common.scss
Normal file
11
src/portal/src/css/common.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
@ -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";
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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='']
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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}
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user