mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-27 04:35:16 +01:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
be6e265890
6
Makefile
6
Makefile
@ -92,7 +92,7 @@ REBUILDCLARITYFLAG=false
|
||||
NEWCLARITYVERSION=
|
||||
|
||||
#clair parameters
|
||||
CLAIRVERSION=v2.0.1
|
||||
CLAIRVERSION=v2.0.1-photon
|
||||
CLAIRFLAG=false
|
||||
CLAIRDBVERSION=9.6.3-photon
|
||||
|
||||
@ -243,7 +243,7 @@ ifeq ($(NOTARYFLAG), true)
|
||||
DOCKERCOMPOSE_LIST+= -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSENOTARYFILENAME)
|
||||
endif
|
||||
ifeq ($(CLAIRFLAG), true)
|
||||
DOCKERSAVE_PARA+= quay.io/coreos/clair:$(CLAIRVERSION) vmware/postgresql:$(CLAIRDBVERSION)
|
||||
DOCKERSAVE_PARA+= vmware/clair:$(CLAIRVERSION) vmware/postgresql:$(CLAIRDBVERSION)
|
||||
PACKAGE_OFFLINE_PARA+= $(HARBORPKG)/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
PACKAGE_ONLINE_PARA+= $(HARBORPKG)/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
DOCKERCOMPOSE_LIST+= -f $(DOCKERCOMPOSEFILEPATH)/$(DOCKERCOMPOSECLAIRFILENAME)
|
||||
@ -368,7 +368,7 @@ package_offline: compile build modify_sourcefiles modify_composefile
|
||||
fi
|
||||
@if [ "$(CLAIRFLAG)" = "true" ] ; then \
|
||||
echo "pulling claiy and postgres..."; \
|
||||
$(DOCKERPULL) quay.io/coreos/clair:$(CLAIRVERSION); \
|
||||
$(DOCKERPULL) vmware/clair:$(CLAIRVERSION); \
|
||||
$(DOCKERPULL) vmware/postgresql:$(CLAIRDBVERSION); \
|
||||
fi
|
||||
|
||||
|
@ -181,6 +181,7 @@ create table img_scan_job (
|
||||
);
|
||||
|
||||
create table img_scan_overview (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
image_digest varchar(128) NOT NULL,
|
||||
scan_job_id int NOT NULL,
|
||||
/* 0 indicates none, the higher the number, the more severe the status */
|
||||
@ -191,7 +192,8 @@ create table img_scan_overview (
|
||||
details_key varchar(128),
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(image_digest)
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE(image_digest)
|
||||
);
|
||||
|
||||
create table clair_vuln_timestamp (
|
||||
|
@ -172,7 +172,8 @@ create table img_scan_job (
|
||||
);
|
||||
|
||||
create table img_scan_overview (
|
||||
image_digest varchar(128) PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY,
|
||||
image_digest varchar(128),
|
||||
scan_job_id int NOT NULL,
|
||||
/* 0 indicates none, the higher the number, the more severe the status */
|
||||
severity int NOT NULL default 0,
|
||||
@ -181,7 +182,8 @@ create table img_scan_overview (
|
||||
/* primary key for querying details, in clair it should be the name of the "top layer" */
|
||||
details_key varchar(128),
|
||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||
update_time timestamp default CURRENT_TIMESTAMP
|
||||
update_time timestamp default CURRENT_TIMESTAMP,
|
||||
UNIQUE(image_digest)
|
||||
);
|
||||
|
||||
CREATE INDEX policy ON replication_job (policy_id);
|
||||
|
@ -35,7 +35,7 @@ services:
|
||||
networks:
|
||||
- harbor-clair
|
||||
container_name: clair
|
||||
image: quay.io/coreos/clair:v2.0.1
|
||||
image: vmware/clair:v2.0.1-photon
|
||||
restart: always
|
||||
depends_on:
|
||||
- postgres
|
||||
|
13
make/photon/clair/Dockerfile
Normal file
13
make/photon/clair/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM library/photon:1.0
|
||||
|
||||
RUN tdnf install -y git bzr rpm xz \
|
||||
&& mkdir /clair2.0.1/
|
||||
|
||||
COPY clair /clair2.0.1/
|
||||
|
||||
VOLUME /config
|
||||
EXPOSE 6060 6061
|
||||
|
||||
RUN chmod u+x /clair2.0.1/clair
|
||||
|
||||
ENTRYPOINT ["/clair2.0.1/clair"]
|
@ -23,6 +23,7 @@ import (
|
||||
"github.com/astaxie/beego/validation"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
http_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/auth"
|
||||
|
||||
@ -80,6 +81,21 @@ func (b *BaseAPI) HandleInternalServerError(text string) {
|
||||
b.RenderError(http.StatusInternalServerError, "")
|
||||
}
|
||||
|
||||
// ParseAndHandleError : if the err is an instance of utils/error.Error,
|
||||
// return the status code and the detail message contained in err, otherwise
|
||||
// return 500
|
||||
func (b *BaseAPI) ParseAndHandleError(text string, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.Errorf("%s: %v", text, err)
|
||||
if e, ok := err.(*http_error.HTTPError); ok {
|
||||
b.RenderError(e.StatusCode, e.Detail)
|
||||
return
|
||||
}
|
||||
b.RenderError(http.StatusInternalServerError, "")
|
||||
}
|
||||
|
||||
// Render returns nil as it won't render template
|
||||
func (b *BaseAPI) Render() error {
|
||||
return nil
|
||||
|
@ -66,4 +66,6 @@ const (
|
||||
WithNotary = "with_notary"
|
||||
WithClair = "with_clair"
|
||||
ScanAllPolicy = "scan_all_policy"
|
||||
|
||||
DefaultClairEndpoint = "http://clair:6060"
|
||||
)
|
||||
|
@ -25,8 +25,12 @@ import (
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// NonExistUserID : if a user does not exist, the ID of the user will be 0.
|
||||
const NonExistUserID = 0
|
||||
const (
|
||||
// NonExistUserID : if a user does not exist, the ID of the user will be 0.
|
||||
NonExistUserID = 0
|
||||
// ClairDBAlias ...
|
||||
ClairDBAlias = "clair-db"
|
||||
)
|
||||
|
||||
// Database is an interface of different databases
|
||||
type Database interface {
|
||||
@ -38,6 +42,24 @@ type Database interface {
|
||||
Register(alias ...string) error
|
||||
}
|
||||
|
||||
// InitClairDB ...
|
||||
func InitClairDB() error {
|
||||
//TODO: Read from env vars.
|
||||
p := &pgsql{
|
||||
host: "postgres",
|
||||
port: 5432,
|
||||
usr: "postgres",
|
||||
pwd: "password",
|
||||
database: "postgres",
|
||||
sslmode: false,
|
||||
}
|
||||
if err := p.Register(ClairDBAlias); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("initialized clair databas")
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitDatabase initializes the database
|
||||
func InitDatabase(database *models.Database) error {
|
||||
db, err := getDatabase(database)
|
||||
|
73
src/common/dao/clair/base.go
Normal file
73
src/common/dao/clair/base.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clair
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
)
|
||||
|
||||
const (
|
||||
updaterLast = "updater/last"
|
||||
)
|
||||
|
||||
var (
|
||||
ormer orm.Ormer
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
//GetOrmer return the singleton of Ormer for clair DB.
|
||||
func GetOrmer() orm.Ormer {
|
||||
once.Do(func() {
|
||||
dbInstance, err := orm.GetDB(dao.ClairDBAlias)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ormer, err = orm.NewOrmWithDB("postgres", dao.ClairDBAlias, dbInstance)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
return ormer
|
||||
}
|
||||
|
||||
//GetLastUpdate query the table `keyvalue` in clair's DB return the value of `updater/last`
|
||||
func GetLastUpdate() (int64, error) {
|
||||
var list orm.ParamsList
|
||||
num, err := GetOrmer().Raw("SELECT value from keyvalue where key=?", updaterLast).ValuesFlat(&list)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if num == 1 {
|
||||
s, ok := list[0].(string)
|
||||
if !ok { // shouldn't be here.
|
||||
return 0, fmt.Errorf("The value: %v, is non-string", list[0])
|
||||
}
|
||||
res, err := strconv.ParseInt(s, 0, 64)
|
||||
if err != nil { //shouldn't be here.
|
||||
return 0, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
if num > 1 {
|
||||
return 0, fmt.Errorf("Multiple entries for %s in Clair DB", updaterLast)
|
||||
}
|
||||
//num is zero, it's not updated yet.
|
||||
return 0, nil
|
||||
}
|
@ -1763,3 +1763,14 @@ func TestVulnTimestamp(t *testing.T) {
|
||||
t.Errorf("Delta should be larger than 2 seconds! old: %v, lastupdate: %v", old, res[0].LastUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListScanOverviews(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := ClearTable(models.ScanOverviewTable)
|
||||
assert.Nil(err)
|
||||
l, err := ListImgScanOverviews()
|
||||
assert.Nil(err)
|
||||
assert.Equal(0, len(l))
|
||||
err = ClearTable(models.ScanOverviewTable)
|
||||
assert.Nil(err)
|
||||
}
|
||||
|
72
src/common/dao/pgsql.go
Normal file
72
src/common/dao/pgsql.go
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/astaxie/beego/orm"
|
||||
_ "github.com/lib/pq" //register pgsql driver
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
)
|
||||
|
||||
type pgsql struct {
|
||||
host string
|
||||
port int
|
||||
usr string
|
||||
pwd string
|
||||
database string
|
||||
sslmode bool
|
||||
}
|
||||
|
||||
type pgsqlSSLMode bool
|
||||
|
||||
func (pm pgsqlSSLMode) String() string {
|
||||
if bool(pm) {
|
||||
return "enable"
|
||||
}
|
||||
return "disable"
|
||||
}
|
||||
|
||||
// Name returns the name of PostgreSQL
|
||||
func (p *pgsql) Name() string {
|
||||
return "PostgreSQL"
|
||||
}
|
||||
|
||||
// String ...
|
||||
func (p *pgsql) String() string {
|
||||
return fmt.Sprintf("type-%s host-%s port-%d databse-%s sslmode-%q",
|
||||
p.Name(), p.host, p.port, p.database, pgsqlSSLMode(p.sslmode))
|
||||
}
|
||||
|
||||
//Register registers pgSQL to orm with the info wrapped by the instance.
|
||||
func (p *pgsql) Register(alias ...string) error {
|
||||
if err := utils.TestTCPConn(fmt.Sprintf("%s:%d", p.host, p.port), 60, 2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := orm.RegisterDriver("postgres", orm.DRPostgres); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
an := "default"
|
||||
if len(alias) != 0 {
|
||||
an = alias[0]
|
||||
}
|
||||
info := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
p.host, p.port, p.usr, p.pwd, p.database, pgsqlSSLMode(p.sslmode))
|
||||
|
||||
return orm.RegisterDataBase(an, "postgres", info)
|
||||
}
|
@ -79,10 +79,13 @@ func IncreasePullCount(name string) (err error) {
|
||||
"pull_count": orm.ColValue(orm.ColAdd, 1),
|
||||
"update_time": time.Now(),
|
||||
})
|
||||
if num == 0 {
|
||||
err = fmt.Errorf("Failed to increase repository pull count with name: %s %s", name, err.Error())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
if num == 0 {
|
||||
return fmt.Errorf("Failed to increase repository pull count with name: %s", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//RepositoryExists returns whether the repository exists according to its name.
|
||||
|
@ -95,6 +95,7 @@ func SetScanJobForImg(digest string, jobID int64) error {
|
||||
}
|
||||
if !created {
|
||||
rec.JobID = jobID
|
||||
rec.UpdateTime = time.Now()
|
||||
n, err := o.Update(rec, "JobID", "UpdateTime")
|
||||
if n == 0 {
|
||||
return fmt.Errorf("Failed to set scan job for image with digest: %s, error: %v", digest, err)
|
||||
@ -105,17 +106,18 @@ func SetScanJobForImg(digest string, jobID int64) error {
|
||||
|
||||
// GetImgScanOverview returns the ImgScanOverview based on the digest.
|
||||
func GetImgScanOverview(digest string) (*models.ImgScanOverview, error) {
|
||||
o := GetOrmer()
|
||||
rec := &models.ImgScanOverview{
|
||||
Digest: digest,
|
||||
}
|
||||
err := o.Read(rec)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
res := []*models.ImgScanOverview{}
|
||||
_, err := scanOverviewQs().Filter("image_digest", digest).All(&res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err == orm.ErrNoRows {
|
||||
if len(res) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(res) > 1 {
|
||||
return nil, fmt.Errorf("Found multiple scan_overview entries for digest: %s", digest)
|
||||
}
|
||||
rec := res[0]
|
||||
if len(rec.CompOverviewStr) > 0 {
|
||||
co := &models.ComponentsOverview{}
|
||||
if err := json.Unmarshal([]byte(rec.CompOverviewStr), co); err != nil {
|
||||
@ -129,20 +131,38 @@ func GetImgScanOverview(digest string) (*models.ImgScanOverview, error) {
|
||||
// UpdateImgScanOverview updates the serverity and components status of a record in img_scan_overview
|
||||
func UpdateImgScanOverview(digest, detailsKey string, sev models.Severity, compOverview *models.ComponentsOverview) error {
|
||||
o := GetOrmer()
|
||||
rec, err := GetImgScanOverview(digest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to getting scan_overview record for update: %v", err)
|
||||
}
|
||||
if rec == nil {
|
||||
return fmt.Errorf("No scan_overview record for digest: %s", digest)
|
||||
}
|
||||
b, err := json.Marshal(compOverview)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rec := &models.ImgScanOverview{
|
||||
Digest: digest,
|
||||
Sev: int(sev),
|
||||
CompOverviewStr: string(b),
|
||||
DetailsKey: detailsKey,
|
||||
UpdateTime: time.Now(),
|
||||
}
|
||||
rec.Sev = int(sev)
|
||||
rec.CompOverviewStr = string(b)
|
||||
rec.DetailsKey = detailsKey
|
||||
rec.UpdateTime = time.Now()
|
||||
|
||||
n, err := o.Update(rec, "Sev", "CompOverviewStr", "DetailsKey", "UpdateTime")
|
||||
if n == 0 || err != nil {
|
||||
return fmt.Errorf("Failed to update scan overview record with digest: %s, error: %v", digest, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListImgScanOverviews list all records in table img_scan_overview, it is called in notificaiton handler when it needs to refresh the severity of all images.
|
||||
func ListImgScanOverviews() ([]*models.ImgScanOverview, error) {
|
||||
var res []*models.ImgScanOverview
|
||||
o := GetOrmer()
|
||||
_, err := o.QueryTable(models.ScanOverviewTable).All(&res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func scanOverviewQs() orm.QuerySeter {
|
||||
o := GetOrmer()
|
||||
return o.QueryTable(models.ScanOverviewTable)
|
||||
}
|
||||
|
@ -23,9 +23,10 @@ const ClairVulnTimestampTable = "clair_vuln_timestamp"
|
||||
|
||||
// ClairVulnTimestamp represents a record in DB that tracks the timestamp of vulnerability in Clair.
|
||||
type ClairVulnTimestamp struct {
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"-"`
|
||||
Namespace string `orm:"column(namespace)" json:"namespace"`
|
||||
LastUpdate time.Time `orm:"column(last_update)" json:"last_update"`
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"-"`
|
||||
Namespace string `orm:"column(namespace)" json:"namespace"`
|
||||
LastUpdate time.Time `orm:"column(last_update)" json:"-"`
|
||||
LastUpdateUTC int64 `orm:"-" json:"last_update"`
|
||||
}
|
||||
|
||||
//TableName is required by beego to map struct to table.
|
||||
@ -107,3 +108,17 @@ type ClairOrderedLayerName struct {
|
||||
Index int `json:"Index"`
|
||||
LayerName string `json:"LayerName"`
|
||||
}
|
||||
|
||||
//ClairVulnerabilityStatus reflects the readiness and freshness of vulnerability data in Clair,
|
||||
//which will be returned in response of systeminfo API.
|
||||
type ClairVulnerabilityStatus struct {
|
||||
OverallUTC int64 `json:"overall_last_update,omitempty"`
|
||||
Details []ClairNamespaceTimestamp `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
//ClairNamespaceTimestamp is a record to store the clairname space and the timestamp,
|
||||
//in practice different namespace in Clair maybe merged into one, e.g. ubuntu:14.04 and ubuntu:16.4 maybe merged into ubuntu and put into response.
|
||||
type ClairNamespaceTimestamp struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Timestamp int64 `json:"last_update"`
|
||||
}
|
||||
|
@ -53,7 +53,8 @@ func (s *ScanJob) TableName() string {
|
||||
|
||||
//ImgScanOverview mapped to a record of image scan overview.
|
||||
type ImgScanOverview struct {
|
||||
Digest string `orm:"pk;column(image_digest)" json:"image_digest"`
|
||||
ID int64 `orm:"pk;auto;column(id)" json:"-"`
|
||||
Digest string `orm:"column(image_digest)" json:"image_digest"`
|
||||
Status string `orm:"-" json:"scan_status"`
|
||||
JobID int64 `orm:"column(scan_job_id)" json:"job_id"`
|
||||
Sev int `orm:"column(severity)" json:"severity"`
|
||||
|
15
src/common/notifier/notification_handler.go
Normal file
15
src/common/notifier/notification_handler.go
Normal file
@ -0,0 +1,15 @@
|
||||
package notifier
|
||||
|
||||
//NotificationHandler defines what operations a notification handler
|
||||
//should have.
|
||||
type NotificationHandler interface {
|
||||
//Handle the event when it coming.
|
||||
//value might be optional, it depends on usages.
|
||||
Handle(value interface{}) error
|
||||
|
||||
//IsStateful returns whether the handler is stateful or not.
|
||||
//If handler is stateful, it will not be triggerred in parallel.
|
||||
//Otherwise, the handler will be triggered concurrently if more
|
||||
//than one same handler are matched the topics.
|
||||
IsStateful() bool
|
||||
}
|
228
src/common/notifier/notifier.go
Normal file
228
src/common/notifier/notifier.go
Normal file
@ -0,0 +1,228 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
//HandlerIndexer is setup the relationship between the handler type and
|
||||
//instance.
|
||||
type HandlerIndexer map[string]NotificationHandler
|
||||
|
||||
//Notification wraps the topic and related data value if existing.
|
||||
type Notification struct {
|
||||
//Topic of notification
|
||||
//Required
|
||||
Topic string
|
||||
|
||||
//Value of notification.
|
||||
//Optional
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
//HandlerChannel provides not only the chan itself but also the count of
|
||||
//handlers related with this chan.
|
||||
type HandlerChannel struct {
|
||||
//To indicate how many handler instances bound with this chan.
|
||||
boundCount uint32
|
||||
|
||||
//The chan for controling concurrent executions.
|
||||
channel chan bool
|
||||
}
|
||||
|
||||
//NotificationWatcher is defined to accept the events published
|
||||
//by the sender and match it with pre-registered notification handler
|
||||
//and then trigger the execution of the found handler.
|
||||
type NotificationWatcher struct {
|
||||
//For handle concurrent scenario.
|
||||
*sync.RWMutex
|
||||
|
||||
//To keep the registered handlers in memory.
|
||||
//Each topic can register multiple handlers.
|
||||
//Each handler can bind to multiple topics.
|
||||
handlers map[string]HandlerIndexer
|
||||
|
||||
//Keep the channels which are used to control the concurrent executions
|
||||
//of multiple stateful handlers with same type.
|
||||
handlerChannels map[string]*HandlerChannel
|
||||
}
|
||||
|
||||
//notificationWatcher is a default notification watcher in package level.
|
||||
var notificationWatcher = NewNotificationWatcher()
|
||||
|
||||
//NewNotificationWatcher is constructor of NotificationWatcher.
|
||||
func NewNotificationWatcher() *NotificationWatcher {
|
||||
return &NotificationWatcher{
|
||||
new(sync.RWMutex),
|
||||
make(map[string]HandlerIndexer),
|
||||
make(map[string]*HandlerChannel),
|
||||
}
|
||||
}
|
||||
|
||||
//Handle the related topic with the specified handler.
|
||||
func (nw *NotificationWatcher) Handle(topic string, handler NotificationHandler) error {
|
||||
if strings.TrimSpace(topic) == "" {
|
||||
return errors.New("Empty topic is not supported")
|
||||
}
|
||||
|
||||
if handler == nil {
|
||||
return errors.New("Nil handler can not be registered")
|
||||
}
|
||||
|
||||
defer nw.Unlock()
|
||||
nw.Lock()
|
||||
|
||||
t := reflect.TypeOf(handler).String()
|
||||
if indexer, ok := nw.handlers[topic]; ok {
|
||||
if _, existing := indexer[t]; existing {
|
||||
return fmt.Errorf("Topic %s has already register the handler with type %s", topic, t)
|
||||
}
|
||||
|
||||
indexer[t] = handler
|
||||
} else {
|
||||
newIndexer := make(HandlerIndexer)
|
||||
newIndexer[t] = handler
|
||||
nw.handlers[topic] = newIndexer
|
||||
}
|
||||
|
||||
if handler.IsStateful() {
|
||||
//First time
|
||||
if handlerChan, ok := nw.handlerChannels[t]; !ok {
|
||||
nw.handlerChannels[t] = &HandlerChannel{1, make(chan bool, 1)}
|
||||
} else {
|
||||
//Already have chan, just increase count
|
||||
handlerChan.boundCount++
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//UnHandle is to revoke the registered handler with the specified topic.
|
||||
//'handler' is optional, the type name of the handler. If it's empty value,
|
||||
//then revoke the whole topic, otherwise only revoke the specified handler.
|
||||
func (nw *NotificationWatcher) UnHandle(topic string, handler string) error {
|
||||
if strings.TrimSpace(topic) == "" {
|
||||
return errors.New("Empty topic is not supported")
|
||||
}
|
||||
|
||||
defer nw.Unlock()
|
||||
nw.Lock()
|
||||
|
||||
var revokeHandler = func(indexer HandlerIndexer, handlerType string) bool {
|
||||
//Find the specified one
|
||||
if hd, existing := indexer[handlerType]; existing {
|
||||
delete(indexer, handlerType)
|
||||
if len(indexer) == 0 {
|
||||
//No handler existing, then remove topic
|
||||
delete(nw.handlers, topic)
|
||||
}
|
||||
|
||||
//Update channel counter or remove channel
|
||||
if hd.IsStateful() {
|
||||
if theChan, yes := nw.handlerChannels[handlerType]; yes {
|
||||
theChan.boundCount--
|
||||
if theChan.boundCount == 0 {
|
||||
//Empty, then remove the channel
|
||||
delete(nw.handlerChannels, handlerType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if indexer, ok := nw.handlers[topic]; ok {
|
||||
if strings.TrimSpace(handler) == "" {
|
||||
for t := range indexer {
|
||||
revokeHandler(indexer, t)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Revoke the specified handler.
|
||||
if revokeHandler(indexer, handler) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Failed to revoke handler %s with topic %s", handler, topic)
|
||||
}
|
||||
|
||||
//Notify that notification is coming.
|
||||
func (nw *NotificationWatcher) Notify(notification Notification) error {
|
||||
if strings.TrimSpace(notification.Topic) == "" {
|
||||
return errors.New("Empty topic can not be notified")
|
||||
}
|
||||
|
||||
nw.RLock()
|
||||
defer nw.RUnlock()
|
||||
|
||||
var (
|
||||
indexer HandlerIndexer
|
||||
ok bool
|
||||
handlers = []NotificationHandler{}
|
||||
)
|
||||
if indexer, ok = nw.handlers[notification.Topic]; !ok {
|
||||
return fmt.Errorf("No handlers registered for handling topic %s", notification.Topic)
|
||||
}
|
||||
|
||||
for _, h := range indexer {
|
||||
handlers = append(handlers, h)
|
||||
}
|
||||
|
||||
//Trigger handlers
|
||||
for _, h := range handlers {
|
||||
var handlerChan chan bool
|
||||
|
||||
if h.IsStateful() {
|
||||
t := reflect.TypeOf(h).String()
|
||||
handlerChan = nw.handlerChannels[t].channel
|
||||
}
|
||||
go func(hd NotificationHandler, ch chan bool) {
|
||||
if hd.IsStateful() && ch != nil {
|
||||
ch <- true
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if hd.IsStateful() && ch != nil {
|
||||
<-ch
|
||||
}
|
||||
}()
|
||||
if err := hd.Handle(notification.Value); err != nil {
|
||||
//Currently, we just log the error
|
||||
log.Errorf("Error occurred when triggerring handler %s of topic %s: %s\n", reflect.TypeOf(hd).String(), notification.Topic, err.Error())
|
||||
}
|
||||
}()
|
||||
}(h, handlerChan)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Subscribe is a wrapper utility method for NotificationWatcher.handle()
|
||||
func Subscribe(topic string, handler NotificationHandler) error {
|
||||
return notificationWatcher.Handle(topic, handler)
|
||||
}
|
||||
|
||||
//UnSubscribe is a wrapper utility method for NotificationWatcher.UnHandle()
|
||||
func UnSubscribe(topic string, handler string) error {
|
||||
return notificationWatcher.UnHandle(topic, handler)
|
||||
}
|
||||
|
||||
//Publish is a wrapper utility method for NotificationWatcher.notify()
|
||||
func Publish(topic string, value interface{}) error {
|
||||
return notificationWatcher.Notify(Notification{
|
||||
Topic: topic,
|
||||
Value: value,
|
||||
})
|
||||
}
|
142
src/common/notifier/notifier_test.go
Normal file
142
src/common/notifier/notifier_test.go
Normal file
@ -0,0 +1,142 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var statefulData int
|
||||
|
||||
type fakeStatefulHandler struct {
|
||||
number int
|
||||
}
|
||||
|
||||
func (fsh *fakeStatefulHandler) IsStateful() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (fsh *fakeStatefulHandler) Handle(v interface{}) error {
|
||||
increment := 0
|
||||
if v != nil && reflect.TypeOf(v).Kind() == reflect.Int {
|
||||
increment = v.(int)
|
||||
}
|
||||
statefulData += increment
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeStatelessHandler struct{}
|
||||
|
||||
func (fsh *fakeStatelessHandler) IsStateful() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (fsh *fakeStatelessHandler) Handle(v interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSubscribeAndUnSubscribe(t *testing.T) {
|
||||
err := Subscribe("topic1", &fakeStatefulHandler{0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = Subscribe("topic1", &fakeStatelessHandler{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = Subscribe("topic2", &fakeStatefulHandler{0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = Subscribe("topic2", &fakeStatelessHandler{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(notificationWatcher.handlers) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if indexer, ok := notificationWatcher.handlers["topic1"]; !ok {
|
||||
t.Fail()
|
||||
} else {
|
||||
if len(indexer) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
if len(notificationWatcher.handlerChannels) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
err = UnSubscribe("topic1", "*notifier.fakeStatefulHandler")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = UnSubscribe("topic2", "*notifier.fakeStatefulHandler")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(notificationWatcher.handlerChannels) != 0 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
err = UnSubscribe("topic1", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(notificationWatcher.handlers) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
err = UnSubscribe("topic2", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(notificationWatcher.handlers) != 0 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
err := Subscribe("topic1", &fakeStatefulHandler{0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = Subscribe("topic2", &fakeStatefulHandler{0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(notificationWatcher.handlers) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
Publish("topic1", 100)
|
||||
Publish("topic2", 50)
|
||||
|
||||
//Waiting for async is done
|
||||
<-time.After(1 * time.Second)
|
||||
|
||||
if statefulData != 150 {
|
||||
t.Fatalf("Expect execution result %d, but got %d", 150, statefulData)
|
||||
}
|
||||
|
||||
err = UnSubscribe("topic1", "*notifier.fakeStatefulHandler")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = UnSubscribe("topic2", "*notifier.fakeStatefulHandler")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
72
src/common/notifier/scan_policy_notitification_handler.go
Normal file
72
src/common/notifier/scan_policy_notitification_handler.go
Normal file
@ -0,0 +1,72 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/scheduler"
|
||||
"github.com/vmware/harbor/src/common/scheduler/policy"
|
||||
"github.com/vmware/harbor/src/common/scheduler/task"
|
||||
)
|
||||
|
||||
const (
|
||||
//PolicyTypeDaily specify the policy type is "daily"
|
||||
PolicyTypeDaily = "daily"
|
||||
|
||||
alternatePolicy = "Alternate Policy"
|
||||
)
|
||||
|
||||
//ScanPolicyNotification is defined for pass the policy change data.
|
||||
type ScanPolicyNotification struct {
|
||||
//Type is used to keep the scan policy type: "none","daily" and "refresh".
|
||||
Type string
|
||||
|
||||
//DailyTime is used when the type is 'daily', the offset with UTC time 00:00.
|
||||
DailyTime int64
|
||||
}
|
||||
|
||||
//ScanPolicyNotificationHandler is defined to handle the changes of scanning
|
||||
//policy.
|
||||
type ScanPolicyNotificationHandler struct{}
|
||||
|
||||
//IsStateful to indicate this handler is stateful.
|
||||
func (s *ScanPolicyNotificationHandler) IsStateful() bool {
|
||||
//Policy change should be done one by one.
|
||||
return true
|
||||
}
|
||||
|
||||
//Handle the policy change notification.
|
||||
func (s *ScanPolicyNotificationHandler) Handle(value interface{}) error {
|
||||
if value == nil {
|
||||
return errors.New("ScanPolicyNotificationHandler can not handle nil value")
|
||||
}
|
||||
|
||||
if reflect.TypeOf(value).Kind() != reflect.Struct ||
|
||||
reflect.TypeOf(value).String() != "notifier.ScanPolicyNotification" {
|
||||
return errors.New("ScanPolicyNotificationHandler can not handle value with invalid type")
|
||||
}
|
||||
|
||||
notification := value.(ScanPolicyNotification)
|
||||
|
||||
hasScheduled := scheduler.DefaultScheduler.HasScheduled(alternatePolicy)
|
||||
if notification.Type == PolicyTypeDaily {
|
||||
if !hasScheduled {
|
||||
schedulePolicy := policy.NewAlternatePolicy(&policy.AlternatePolicyConfiguration{
|
||||
Duration: 24 * time.Hour,
|
||||
OffsetTime: notification.DailyTime,
|
||||
})
|
||||
attachTask := task.NewScanAllTask()
|
||||
schedulePolicy.AttachTasks(attachTask)
|
||||
|
||||
return scheduler.DefaultScheduler.Schedule(schedulePolicy)
|
||||
}
|
||||
} else {
|
||||
if hasScheduled {
|
||||
return scheduler.DefaultScheduler.UnSchedule(alternatePolicy)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/scheduler"
|
||||
)
|
||||
|
||||
var testingScheduler = scheduler.DefaultScheduler
|
||||
|
||||
func TestScanPolicyNotificationHandler(t *testing.T) {
|
||||
//Scheduler should be running.
|
||||
testingScheduler.Start()
|
||||
if !testingScheduler.IsRunning() {
|
||||
t.Fatal("scheduler should be running")
|
||||
}
|
||||
|
||||
handler := &ScanPolicyNotificationHandler{}
|
||||
if !handler.IsStateful() {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
utcTime := time.Now().UTC().Unix()
|
||||
notification := ScanPolicyNotification{"daily", utcTime + 3600}
|
||||
if err := handler.Handle(notification); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//Waiting for everything is ready.
|
||||
<-time.After(1 * time.Second)
|
||||
if !testingScheduler.HasScheduled("Alternate Policy") {
|
||||
t.Fatal("Handler does not work")
|
||||
}
|
||||
|
||||
notification2 := ScanPolicyNotification{"none", 0}
|
||||
if err := handler.Handle(notification2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//Waiting for everything is ready.
|
||||
<-time.After(1 * time.Second)
|
||||
if testingScheduler.HasScheduled("Alternate Policy") {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
//Clear
|
||||
testingScheduler.Stop()
|
||||
//Waiting for everything is ready.
|
||||
<-time.After(1 * time.Second)
|
||||
if testingScheduler.IsRunning() {
|
||||
t.Fatal("scheduler should be stopped")
|
||||
}
|
||||
}
|
11
src/common/notifier/topics.go
Normal file
11
src/common/notifier/topics.go
Normal file
@ -0,0 +1,11 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common"
|
||||
)
|
||||
|
||||
//Define global topic names
|
||||
const (
|
||||
//ScanAllPolicyTopic is for notifying the change of scanning all policy.
|
||||
ScanAllPolicyTopic = common.ScanAllPolicy
|
||||
)
|
154
src/common/scheduler/policy/alternate_policy.go
Normal file
154
src/common/scheduler/policy/alternate_policy.go
Normal file
@ -0,0 +1,154 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/scheduler/task"
|
||||
)
|
||||
|
||||
//AlternatePolicyConfiguration store the related configurations for alternate policy.
|
||||
type AlternatePolicyConfiguration struct {
|
||||
//Duration is the interval of executing attached tasks.
|
||||
Duration time.Duration
|
||||
|
||||
//OffsetTime is the execution time point of each turn
|
||||
//It's a number to indicate the seconds offset to the 00:00 of UTC time.
|
||||
OffsetTime int64
|
||||
}
|
||||
|
||||
//AlternatePolicy is a policy that repeatedly executing tasks with specified duration during a specified time scope.
|
||||
type AlternatePolicy struct {
|
||||
//Keep the attached tasks.
|
||||
tasks []task.Task
|
||||
|
||||
//Policy configurations.
|
||||
config *AlternatePolicyConfiguration
|
||||
|
||||
//Generate time ticks with specified duration.
|
||||
ticker *time.Ticker
|
||||
|
||||
//To indicated whether policy is completed.
|
||||
isEnabled bool
|
||||
|
||||
//Channel used to send evaluation result signals.
|
||||
evaluation chan bool
|
||||
|
||||
//Channel used to notify policy termination.
|
||||
done chan bool
|
||||
|
||||
//Channel used to receive terminate signal.
|
||||
terminator chan bool
|
||||
}
|
||||
|
||||
//NewAlternatePolicy is constructor of creating AlternatePolicy.
|
||||
func NewAlternatePolicy(config *AlternatePolicyConfiguration) *AlternatePolicy {
|
||||
return &AlternatePolicy{
|
||||
tasks: []task.Task{},
|
||||
config: config,
|
||||
isEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
//GetConfig returns the current configuration options of this policy.
|
||||
func (alp *AlternatePolicy) GetConfig() *AlternatePolicyConfiguration {
|
||||
return alp.config
|
||||
}
|
||||
|
||||
//Name is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Name() string {
|
||||
return "Alternate Policy"
|
||||
}
|
||||
|
||||
//Tasks is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Tasks() []task.Task {
|
||||
copyList := []task.Task{}
|
||||
if alp.tasks != nil && len(alp.tasks) > 0 {
|
||||
copyList = append(copyList, alp.tasks...)
|
||||
}
|
||||
|
||||
return copyList
|
||||
}
|
||||
|
||||
//Done is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Done() <-chan bool {
|
||||
return alp.done
|
||||
}
|
||||
|
||||
//AttachTasks is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) AttachTasks(tasks ...task.Task) error {
|
||||
if tasks == nil || len(tasks) == 0 {
|
||||
return errors.New("No tasks can be attached")
|
||||
}
|
||||
|
||||
alp.tasks = append(alp.tasks, tasks...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Disable is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Disable() error {
|
||||
//Stop the ticker
|
||||
if alp.ticker != nil {
|
||||
alp.ticker.Stop()
|
||||
}
|
||||
|
||||
//Stop the evaluation goroutine
|
||||
alp.terminator <- true
|
||||
alp.ticker = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Evaluate is an implementation of same method in policy interface.
|
||||
func (alp *AlternatePolicy) Evaluate() (<-chan bool, error) {
|
||||
//Keep idempotent
|
||||
if alp.isEnabled && alp.evaluation != nil {
|
||||
return alp.evaluation, nil
|
||||
}
|
||||
|
||||
alp.done = make(chan bool)
|
||||
alp.terminator = make(chan bool)
|
||||
alp.evaluation = make(chan bool)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
alp.isEnabled = false
|
||||
}()
|
||||
timeNow := time.Now().UTC()
|
||||
|
||||
//Reach the execution time point?
|
||||
utcTime := (int64)(timeNow.Hour()*3600 + timeNow.Minute()*60)
|
||||
diff := alp.config.OffsetTime - utcTime
|
||||
if diff < 0 {
|
||||
diff += 24 * 3600
|
||||
}
|
||||
if diff > 0 {
|
||||
//Wait for a while.
|
||||
select {
|
||||
case <-time.After(time.Duration(diff) * time.Second):
|
||||
case <-alp.terminator:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Trigger the first tick.
|
||||
alp.evaluation <- true
|
||||
|
||||
//Start the ticker for repeat checking.
|
||||
alp.ticker = time.NewTicker(alp.config.Duration)
|
||||
for {
|
||||
select {
|
||||
case <-alp.ticker.C:
|
||||
alp.evaluation <- true
|
||||
case <-alp.terminator:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
//Enabled
|
||||
alp.isEnabled = true
|
||||
|
||||
return alp.evaluation, nil
|
||||
}
|
114
src/common/scheduler/policy/alternate_policy_test.go
Normal file
114
src/common/scheduler/policy/alternate_policy_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeTask struct {
|
||||
number int
|
||||
}
|
||||
|
||||
func (ft *fakeTask) Name() string {
|
||||
return "for testing"
|
||||
}
|
||||
|
||||
func (ft *fakeTask) Run() error {
|
||||
ft.number++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
tp := NewAlternatePolicy(&AlternatePolicyConfiguration{})
|
||||
err := tp.AttachTasks(&fakeTask{number: 100})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if tp.GetConfig() == nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if tp.Name() != "Alternate Policy" {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
tks := tp.Tasks()
|
||||
if tks == nil || len(tks) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEvaluatePolicy(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
utcOffset := (int64)(now.Hour()*3600 + now.Minute()*60)
|
||||
tp := NewAlternatePolicy(&AlternatePolicyConfiguration{
|
||||
Duration: 1 * time.Second,
|
||||
OffsetTime: utcOffset + 1,
|
||||
})
|
||||
err := tp.AttachTasks(&fakeTask{number: 100})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
ch, _ := tp.Evaluate()
|
||||
counter := 0
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
select {
|
||||
case <-ch:
|
||||
counter++
|
||||
case <-time.After(2 * time.Second):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if counter != 3 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
tp.Disable()
|
||||
}
|
||||
|
||||
func TestDisablePolicy(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
utcOffset := (int64)(now.Hour()*3600 + now.Minute()*60)
|
||||
tp := NewAlternatePolicy(&AlternatePolicyConfiguration{
|
||||
Duration: 1 * time.Second,
|
||||
OffsetTime: utcOffset + 1,
|
||||
})
|
||||
err := tp.AttachTasks(&fakeTask{number: 100})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
ch, _ := tp.Evaluate()
|
||||
counter := 0
|
||||
terminate := make(chan bool)
|
||||
defer func() {
|
||||
terminate <- true
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ch:
|
||||
counter++
|
||||
case <-terminate:
|
||||
return
|
||||
case <-time.After(6 * time.Second):
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
time.Sleep(2 * time.Second)
|
||||
if tp.Disable() != nil {
|
||||
t.Fatal("Failed to disable policy")
|
||||
}
|
||||
//Waiting for everything is stable
|
||||
<-time.After(1 * time.Second)
|
||||
//Copy value
|
||||
copiedCounter := counter
|
||||
time.Sleep(2 * time.Second)
|
||||
if counter != copiedCounter {
|
||||
t.Fatalf("Policy is still running after calling Disable() %d=%d", copiedCounter, counter)
|
||||
}
|
||||
}
|
39
src/common/scheduler/policy/policy.go
Normal file
39
src/common/scheduler/policy/policy.go
Normal file
@ -0,0 +1,39 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common/scheduler/task"
|
||||
)
|
||||
|
||||
//Policy is an if-then logic to determine how the attached tasks should be
|
||||
//executed based on the evaluation result of the defined conditions.
|
||||
//E.g:
|
||||
// Daily execute TASK between 2017/06/24 and 2018/06/23
|
||||
// Execute TASK at 2017/09/01 14:30:00
|
||||
//
|
||||
//Each policy should have a name to identify itself.
|
||||
//Please be aware that policy with no tasks will be treated as invalid.
|
||||
//
|
||||
type Policy interface {
|
||||
//Name will return the name of the policy.
|
||||
Name() string
|
||||
|
||||
//Tasks will return the attached tasks with this policy.
|
||||
Tasks() []task.Task
|
||||
|
||||
//AttachTasks is to attach tasks to this policy
|
||||
AttachTasks(...task.Task) error
|
||||
|
||||
//Done will setup a channel for other components to check whether or not
|
||||
//the policy is completed. Possibly designed for the none loop policy.
|
||||
Done() <-chan bool
|
||||
|
||||
//Evaluate the policy based on its definition and return the result via
|
||||
//result channel. Policy is enabled after it is evaluated.
|
||||
//Make sure Evaluate is idempotent, that means one policy can be only enabled
|
||||
//only once even if Evaluate is called more than one times.
|
||||
Evaluate() (<-chan bool, error)
|
||||
|
||||
//Disable the enabled policy and release all the allocated resources.
|
||||
//Disable should also send signal to the terminated channel which returned by Done.
|
||||
Disable() error
|
||||
}
|
265
src/common/scheduler/scheduler.go
Normal file
265
src/common/scheduler/scheduler.go
Normal file
@ -0,0 +1,265 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common/scheduler/policy"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultQueueSize = 10
|
||||
|
||||
statSchedulePolicy = "Schedule Policy"
|
||||
statUnSchedulePolicy = "Unschedule Policy"
|
||||
statTaskRun = "Task Run"
|
||||
statTaskComplete = "Task Complete"
|
||||
statTaskFail = "Task Fail"
|
||||
)
|
||||
|
||||
//StatItem is defined for the stat metrics.
|
||||
type StatItem struct {
|
||||
//Metrics catalog
|
||||
Type string
|
||||
|
||||
//The stat value
|
||||
Value uint32
|
||||
|
||||
//Attach some other info
|
||||
Attachment interface{}
|
||||
}
|
||||
|
||||
//StatSummary is used to collect some metrics of scheduler.
|
||||
type StatSummary struct {
|
||||
//Count of scheduled policy
|
||||
PolicyCount uint32
|
||||
|
||||
//Total count of tasks
|
||||
Tasks uint32
|
||||
|
||||
//Count of successfully complete tasks
|
||||
CompletedTasks uint32
|
||||
|
||||
//Count of tasks with errors
|
||||
TasksWithError uint32
|
||||
}
|
||||
|
||||
//Configuration defines configuration of Scheduler.
|
||||
type Configuration struct {
|
||||
QueueSize uint8
|
||||
}
|
||||
|
||||
//Scheduler is designed for scheduling policies.
|
||||
type Scheduler struct {
|
||||
//Related configuration options for scheduler.
|
||||
config *Configuration
|
||||
|
||||
//Store to keep the references of scheduled policies.
|
||||
policies Store
|
||||
|
||||
//Queue for accepting the scheduling polices.
|
||||
scheduleQueue chan policy.Policy
|
||||
|
||||
//Queue for receiving policy unschedule request or complete signal.
|
||||
unscheduleQueue chan string
|
||||
|
||||
//Channel for receiving stat metrics.
|
||||
statChan chan *StatItem
|
||||
|
||||
//Channel for terminate scheduler damon.
|
||||
terminateChan chan bool
|
||||
|
||||
//The stat metrics of scheduler.
|
||||
stats *StatSummary
|
||||
|
||||
//To indicate whether scheduler is running or not
|
||||
isRunning bool
|
||||
}
|
||||
|
||||
//DefaultScheduler is a default scheduler.
|
||||
var DefaultScheduler = NewScheduler(nil)
|
||||
|
||||
//NewScheduler is constructor for creating a scheduler.
|
||||
func NewScheduler(config *Configuration) *Scheduler {
|
||||
var qSize uint8 = defaultQueueSize
|
||||
if config != nil && config.QueueSize > 0 {
|
||||
qSize = config.QueueSize
|
||||
}
|
||||
|
||||
sq := make(chan policy.Policy, qSize)
|
||||
usq := make(chan string, qSize)
|
||||
stChan := make(chan *StatItem, 4)
|
||||
tc := make(chan bool, 1)
|
||||
|
||||
store := NewConcurrentStore()
|
||||
return &Scheduler{
|
||||
config: config,
|
||||
policies: store,
|
||||
scheduleQueue: sq,
|
||||
unscheduleQueue: usq,
|
||||
statChan: stChan,
|
||||
terminateChan: tc,
|
||||
stats: &StatSummary{
|
||||
PolicyCount: 0,
|
||||
Tasks: 0,
|
||||
CompletedTasks: 0,
|
||||
TasksWithError: 0,
|
||||
},
|
||||
isRunning: false,
|
||||
}
|
||||
}
|
||||
|
||||
//Start the scheduler damon.
|
||||
func (sch *Scheduler) Start() {
|
||||
if sch.isRunning {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Runtime error in scheduler:%s\n", r)
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
sch.isRunning = false
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-sch.terminateChan:
|
||||
//Exit
|
||||
return
|
||||
case p := <-sch.scheduleQueue:
|
||||
//Schedule the policy.
|
||||
watcher := NewWatcher(p, sch.statChan, sch.unscheduleQueue)
|
||||
|
||||
//Keep the policy for future use after it's successfully scheduled.
|
||||
sch.policies.Put(p.Name(), watcher)
|
||||
|
||||
//Enable it.
|
||||
watcher.Start()
|
||||
|
||||
sch.statChan <- &StatItem{statSchedulePolicy, 1, nil}
|
||||
case name := <-sch.unscheduleQueue:
|
||||
//Find the watcher.
|
||||
watcher := sch.policies.Remove(name)
|
||||
if watcher != nil && watcher.IsRunning() {
|
||||
watcher.Stop()
|
||||
}
|
||||
|
||||
sch.statChan <- &StatItem{statUnSchedulePolicy, 1, nil}
|
||||
|
||||
case stat := <-sch.statChan:
|
||||
{
|
||||
switch stat.Type {
|
||||
case statSchedulePolicy:
|
||||
sch.stats.PolicyCount += stat.Value
|
||||
break
|
||||
case statUnSchedulePolicy:
|
||||
sch.stats.PolicyCount -= stat.Value
|
||||
break
|
||||
case statTaskRun:
|
||||
sch.stats.Tasks += stat.Value
|
||||
break
|
||||
case statTaskComplete:
|
||||
sch.stats.CompletedTasks += stat.Value
|
||||
break
|
||||
case statTaskFail:
|
||||
sch.stats.TasksWithError += stat.Value
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
log.Infof("Policies:%d, Tasks:%d, CompletedTasks:%d, FailedTasks:%d\n",
|
||||
sch.stats.PolicyCount,
|
||||
sch.stats.Tasks,
|
||||
sch.stats.CompletedTasks,
|
||||
sch.stats.TasksWithError)
|
||||
|
||||
if stat.Attachment != nil &&
|
||||
reflect.TypeOf(stat.Attachment).String() == "*errors.errorString" {
|
||||
log.Errorf("%s: %s\n", stat.Type, stat.Attachment.(error).Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
sch.isRunning = true
|
||||
log.Infof("Policy scheduler start at %s\n", time.Now().UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
//Stop the scheduler damon.
|
||||
func (sch *Scheduler) Stop() {
|
||||
if !sch.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
//Terminate damon firstly to stop receiving signals.
|
||||
sch.terminateChan <- true
|
||||
|
||||
//Stop all watchers.
|
||||
for _, wt := range sch.policies.GetAll() {
|
||||
wt.Stop()
|
||||
}
|
||||
|
||||
//Clear resources
|
||||
sch.policies.Clear()
|
||||
|
||||
log.Infof("Policy scheduler stop at %s\n", time.Now().UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
//Schedule and enable the policy.
|
||||
func (sch *Scheduler) Schedule(scheduledPolicy policy.Policy) error {
|
||||
if scheduledPolicy == nil {
|
||||
return errors.New("nil is not Policy object")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(scheduledPolicy.Name()) == "" {
|
||||
return errors.New("Policy should be assigned a name")
|
||||
}
|
||||
|
||||
tasks := scheduledPolicy.Tasks()
|
||||
if tasks == nil || len(tasks) == 0 {
|
||||
return errors.New("Policy must attach task(s)")
|
||||
}
|
||||
|
||||
if sch.policies.Exists(scheduledPolicy.Name()) {
|
||||
return fmt.Errorf("Duplicated policy: %s", scheduledPolicy.Name())
|
||||
}
|
||||
|
||||
//Schedule the policy.
|
||||
sch.scheduleQueue <- scheduledPolicy
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//UnSchedule the specified policy from the enabled policies list.
|
||||
func (sch *Scheduler) UnSchedule(policyName string) error {
|
||||
if strings.TrimSpace(policyName) == "" {
|
||||
return errors.New("Empty policy name is invalid")
|
||||
}
|
||||
|
||||
if !sch.policies.Exists(policyName) {
|
||||
return fmt.Errorf("Policy %s is not existing", policyName)
|
||||
}
|
||||
|
||||
//Unschedule the policy.
|
||||
sch.unscheduleQueue <- policyName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//IsRunning to indicate whether the scheduler is running.
|
||||
func (sch *Scheduler) IsRunning() bool {
|
||||
return sch.isRunning
|
||||
}
|
||||
|
||||
//HasScheduled is to check whether the given policy has been scheduled or not.
|
||||
func (sch *Scheduler) HasScheduled(policyName string) bool {
|
||||
return sch.policies.Exists(policyName)
|
||||
}
|
136
src/common/scheduler/scheduler_store.go
Normal file
136
src/common/scheduler/scheduler_store.go
Normal file
@ -0,0 +1,136 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//Store define the basic operations for storing and managing policy watcher.
|
||||
//The concrete implementation should consider concurrent supporting scenario.
|
||||
//
|
||||
type Store interface {
|
||||
//Put a new policy in.
|
||||
Put(key string, value *Watcher)
|
||||
|
||||
//Get the corresponding policy with the key.
|
||||
Get(key string) *Watcher
|
||||
|
||||
//Exists is to check if the key existing in the store.
|
||||
Exists(key string) bool
|
||||
|
||||
//Remove the specified policy and return its reference.
|
||||
Remove(key string) *Watcher
|
||||
|
||||
//Size return the total count of items in store.
|
||||
Size() uint32
|
||||
|
||||
//GetAll is to get all the items in the store.
|
||||
GetAll() []*Watcher
|
||||
|
||||
//Clear store.
|
||||
Clear()
|
||||
}
|
||||
|
||||
//ConcurrentStore implements Store interface and supports concurrent operations.
|
||||
type ConcurrentStore struct {
|
||||
//Read-write mutex to synchronize the data map.
|
||||
*sync.RWMutex
|
||||
|
||||
//Map used to keep the policy list.
|
||||
data map[string]*Watcher
|
||||
}
|
||||
|
||||
//NewConcurrentStore is used to create a new store and return the pointer reference.
|
||||
func NewConcurrentStore() *ConcurrentStore {
|
||||
mutex := new(sync.RWMutex)
|
||||
data := make(map[string]*Watcher)
|
||||
|
||||
return &ConcurrentStore{mutex, data}
|
||||
}
|
||||
|
||||
//Put a policy into store.
|
||||
func (cs *ConcurrentStore) Put(key string, value *Watcher) {
|
||||
if strings.TrimSpace(key) == "" || value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer cs.Unlock()
|
||||
|
||||
cs.Lock()
|
||||
cs.data[key] = value
|
||||
}
|
||||
|
||||
//Get policy via key.
|
||||
func (cs *ConcurrentStore) Get(key string) *Watcher {
|
||||
if strings.TrimSpace(key) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer cs.RUnlock()
|
||||
|
||||
cs.RLock()
|
||||
return cs.data[key]
|
||||
}
|
||||
|
||||
//Exists is used to check whether or not the key exists in store.
|
||||
func (cs *ConcurrentStore) Exists(key string) bool {
|
||||
if strings.TrimSpace(key) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
defer cs.RUnlock()
|
||||
|
||||
cs.RLock()
|
||||
_, ok := cs.data[key]
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
//Remove is to delete the specified policy.
|
||||
func (cs *ConcurrentStore) Remove(key string) *Watcher {
|
||||
if !cs.Exists(key) {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer cs.Unlock()
|
||||
|
||||
cs.Lock()
|
||||
if wt, ok := cs.data[key]; ok {
|
||||
delete(cs.data, key)
|
||||
return wt
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Size return the total count of items in store.
|
||||
func (cs *ConcurrentStore) Size() uint32 {
|
||||
return (uint32)(len(cs.data))
|
||||
}
|
||||
|
||||
//GetAll to get all the items of store.
|
||||
func (cs *ConcurrentStore) GetAll() []*Watcher {
|
||||
all := []*Watcher{}
|
||||
|
||||
defer cs.RUnlock()
|
||||
cs.RLock()
|
||||
for _, v := range cs.data {
|
||||
all = append(all, v)
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
//Clear all the items in store.
|
||||
func (cs *ConcurrentStore) Clear() {
|
||||
if cs.Size() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
defer cs.Unlock()
|
||||
cs.Lock()
|
||||
|
||||
for k := range cs.data {
|
||||
delete(cs.data, k)
|
||||
}
|
||||
}
|
71
src/common/scheduler/scheduler_store_test.go
Normal file
71
src/common/scheduler/scheduler_store_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPut(t *testing.T) {
|
||||
store := NewConcurrentStore()
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
if store.Size() != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
store := NewConcurrentStore()
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
w := store.Get("testing")
|
||||
if w == nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
store := NewConcurrentStore()
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
if !store.Exists("testing") {
|
||||
t.Fail()
|
||||
}
|
||||
w := store.Remove("testing")
|
||||
if w == nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestExisting(t *testing.T) {
|
||||
store := NewConcurrentStore()
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
if !store.Exists("testing") {
|
||||
t.Fail()
|
||||
}
|
||||
if store.Exists("fake_key") {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAll(t *testing.T) {
|
||||
store := NewConcurrentStore()
|
||||
if store == nil {
|
||||
t.Fatal("Failed to creat store instance")
|
||||
}
|
||||
store.Put("testing", NewWatcher(nil, nil, nil))
|
||||
store.Put("testing2", NewWatcher(nil, nil, nil))
|
||||
list := store.GetAll()
|
||||
if list == nil || len(list) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
142
src/common/scheduler/scheduler_test.go
Normal file
142
src/common/scheduler/scheduler_test.go
Normal file
@ -0,0 +1,142 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/scheduler/task"
|
||||
)
|
||||
|
||||
type fakePolicy struct {
|
||||
tasks []task.Task
|
||||
done chan bool
|
||||
evaluation chan bool
|
||||
terminate chan bool
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Name() string {
|
||||
return "testing policy"
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Tasks() []task.Task {
|
||||
return fp.tasks
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) AttachTasks(tasks ...task.Task) error {
|
||||
fp.tasks = append(fp.tasks, tasks...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Done() <-chan bool {
|
||||
return fp.done
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Evaluate() (<-chan bool, error) {
|
||||
fp.evaluation = make(chan bool, 1)
|
||||
fp.done = make(chan bool)
|
||||
fp.terminate = make(chan bool)
|
||||
|
||||
fp.evaluation <- true
|
||||
go func() {
|
||||
fp.ticker = time.NewTicker(1 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-fp.terminate:
|
||||
return
|
||||
case <-fp.ticker.C:
|
||||
fp.evaluation <- true
|
||||
}
|
||||
}
|
||||
}()
|
||||
return fp.evaluation, nil
|
||||
}
|
||||
|
||||
func (fp *fakePolicy) Disable() error {
|
||||
if fp.ticker != nil {
|
||||
fp.ticker.Stop()
|
||||
}
|
||||
|
||||
fp.terminate <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeTask struct {
|
||||
number int
|
||||
}
|
||||
|
||||
func (ft *fakeTask) Name() string {
|
||||
return "for testing"
|
||||
}
|
||||
|
||||
func (ft *fakeTask) Run() error {
|
||||
ft.number++
|
||||
return nil
|
||||
}
|
||||
|
||||
//Wacher will be tested together with scheduler.
|
||||
func TestScheduler(t *testing.T) {
|
||||
DefaultScheduler.Start()
|
||||
if DefaultScheduler.policies.Size() != 0 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if DefaultScheduler.stats.PolicyCount != 0 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if !DefaultScheduler.IsRunning() {
|
||||
t.Fatal("Scheduler is not started")
|
||||
}
|
||||
|
||||
fp := &fakePolicy{
|
||||
tasks: []task.Task{},
|
||||
}
|
||||
fk := &fakeTask{number: 100}
|
||||
fp.AttachTasks(fk)
|
||||
|
||||
if DefaultScheduler.Schedule(fp) != nil {
|
||||
t.Fatal("Schedule policy failed")
|
||||
}
|
||||
//Waiting for everything is stable
|
||||
time.Sleep(1 * time.Second)
|
||||
if DefaultScheduler.policies.Size() == 0 {
|
||||
t.Fatal("No policy in the store after calling Schedule()")
|
||||
}
|
||||
if DefaultScheduler.stats.PolicyCount != 1 {
|
||||
t.Fatal("Policy stats do not match")
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
if fk.number == 100 {
|
||||
t.Fatal("Task is not triggered")
|
||||
}
|
||||
if DefaultScheduler.stats.Tasks == 0 {
|
||||
t.Fail()
|
||||
}
|
||||
if DefaultScheduler.stats.CompletedTasks == 0 {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if DefaultScheduler.UnSchedule(fp.Name()) != nil {
|
||||
t.Fatal("Unschedule policy failed")
|
||||
}
|
||||
//Waiting for everything is stable
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
if DefaultScheduler.stats.PolicyCount != 0 {
|
||||
t.Fatal("Policy count does not match after calling UnSchedule()")
|
||||
}
|
||||
copiedValue := DefaultScheduler.stats.CompletedTasks
|
||||
<-time.After(2 * time.Second)
|
||||
|
||||
if copiedValue != DefaultScheduler.stats.CompletedTasks {
|
||||
t.Fatalf("Policy is still enabled after calling UnSchedule(),%d=%d", copiedValue, DefaultScheduler.stats.CompletedTasks)
|
||||
}
|
||||
|
||||
DefaultScheduler.Stop()
|
||||
<-time.After(1 * time.Second)
|
||||
if DefaultScheduler.policies.Size() != 0 || DefaultScheduler.IsRunning() {
|
||||
t.Fatal("Scheduler is still running after stopping")
|
||||
}
|
||||
}
|
23
src/common/scheduler/task/scan_all_task.go
Normal file
23
src/common/scheduler/task/scan_all_task.go
Normal file
@ -0,0 +1,23 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/ui/utils"
|
||||
)
|
||||
|
||||
//ScanAllTask is task of scanning all tags.
|
||||
type ScanAllTask struct{}
|
||||
|
||||
//NewScanAllTask is constructor of creating ScanAllTask.
|
||||
func NewScanAllTask() *ScanAllTask {
|
||||
return &ScanAllTask{}
|
||||
}
|
||||
|
||||
//Name returns the name of the task.
|
||||
func (sat *ScanAllTask) Name() string {
|
||||
return "scan all"
|
||||
}
|
||||
|
||||
//Run the actions.
|
||||
func (sat *ScanAllTask) Run() error {
|
||||
return utils.ScanAllImages()
|
||||
}
|
16
src/common/scheduler/task/scan_all_task_test.go
Normal file
16
src/common/scheduler/task/scan_all_task_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTask(t *testing.T) {
|
||||
tk := NewScanAllTask()
|
||||
if tk == nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if tk.Name() != "scan all" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
10
src/common/scheduler/task/task.go
Normal file
10
src/common/scheduler/task/task.go
Normal file
@ -0,0 +1,10 @@
|
||||
package task
|
||||
|
||||
//Task is used to synchronously run specific action(s).
|
||||
type Task interface {
|
||||
//Name should return the name of the task.
|
||||
Name() string
|
||||
|
||||
//Run the concrete code here
|
||||
Run() error
|
||||
}
|
139
src/common/scheduler/watcher.go
Normal file
139
src/common/scheduler/watcher.go
Normal file
@ -0,0 +1,139 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common/scheduler/policy"
|
||||
"github.com/vmware/harbor/src/common/scheduler/task"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//Watcher is an asynchronous runner to provide an evaluation environment for the policy.
|
||||
type Watcher struct {
|
||||
//The target policy.
|
||||
p policy.Policy
|
||||
|
||||
//The channel for receive stop signal.
|
||||
cmdChan chan bool
|
||||
|
||||
//Indicate whether the watch is started and running.
|
||||
isRunning bool
|
||||
|
||||
//Report stats to scheduler.
|
||||
stats chan *StatItem
|
||||
|
||||
//If policy is automatically completed, report the policy to scheduler.
|
||||
doneChan chan string
|
||||
}
|
||||
|
||||
//NewWatcher is used as a constructor.
|
||||
func NewWatcher(p policy.Policy, st chan *StatItem, done chan string) *Watcher {
|
||||
return &Watcher{
|
||||
p: p,
|
||||
cmdChan: make(chan bool),
|
||||
isRunning: false,
|
||||
stats: st,
|
||||
doneChan: done,
|
||||
}
|
||||
}
|
||||
|
||||
//Start the running.
|
||||
func (wc *Watcher) Start() {
|
||||
if wc.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
if wc.p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func(pl policy.Policy) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Runtime error in watcher:%s\n", r)
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
wc.isRunning = false
|
||||
}()
|
||||
|
||||
evalChan, err := pl.Evaluate()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to evaluate ploicy %s with error: %s\n", pl.Name(), err.Error())
|
||||
return
|
||||
}
|
||||
done := pl.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-evalChan:
|
||||
{
|
||||
//Start to run the attached tasks.
|
||||
for _, t := range pl.Tasks() {
|
||||
go func(tk task.Task) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
st := &StatItem{statTaskFail, 1, fmt.Errorf("Runtime error in task execution:%s", r)}
|
||||
if wc.stats != nil {
|
||||
wc.stats <- st
|
||||
}
|
||||
}
|
||||
}()
|
||||
err := tk.Run()
|
||||
|
||||
//Report task execution stats.
|
||||
st := &StatItem{statTaskComplete, 1, err}
|
||||
if err != nil {
|
||||
st.Type = statTaskFail
|
||||
}
|
||||
if wc.stats != nil {
|
||||
wc.stats <- st
|
||||
}
|
||||
}(t)
|
||||
|
||||
//Report task run stats.
|
||||
st := &StatItem{statTaskRun, 1, nil}
|
||||
if wc.stats != nil {
|
||||
wc.stats <- st
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-done:
|
||||
{
|
||||
//Policy is automatically completed.
|
||||
//Report policy change stats.
|
||||
if wc.doneChan != nil {
|
||||
wc.doneChan <- wc.p.Name()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
case <-wc.cmdChan:
|
||||
//Exit goroutine.
|
||||
return
|
||||
}
|
||||
}
|
||||
}(wc.p)
|
||||
|
||||
wc.isRunning = true
|
||||
}
|
||||
|
||||
//Stop the running.
|
||||
func (wc *Watcher) Stop() {
|
||||
if !wc.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
//Disable policy.
|
||||
if wc.p != nil {
|
||||
wc.p.Disable()
|
||||
}
|
||||
//Stop watcher.
|
||||
wc.cmdChan <- true
|
||||
}
|
||||
|
||||
//IsRunning to indicate if the watcher is still running.
|
||||
func (wc *Watcher) IsRunning() bool {
|
||||
return wc.isRunning
|
||||
}
|
@ -26,6 +26,7 @@ import (
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
http_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
@ -192,9 +193,11 @@ func Login(client *http.Client, url, username, password string) (*AuthContext, e
|
||||
func send(client *http.Client, req *http.Request) (*AuthContext, error) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Debugf("\"%s %s\" failed", req.Method, req.URL.String())
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
log.Debugf("\"%s %s\" %d", req.Method, req.URL.String(), resp.StatusCode)
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@ -202,7 +205,10 @@ func send(client *http.Client, req *http.Request) (*AuthContext, error) {
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d %s", resp.StatusCode, string(data))
|
||||
return nil, &http_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(data),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := &AuthContext{}
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
// "path"
|
||||
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
@ -40,7 +41,7 @@ func NewClient(endpoint string, logger *log.Logger) *Client {
|
||||
logger = log.DefaultLogger()
|
||||
}
|
||||
return &Client{
|
||||
endpoint: endpoint,
|
||||
endpoint: strings.TrimSuffix(endpoint, "/"),
|
||||
logger: logger,
|
||||
client: &http.Client{},
|
||||
}
|
||||
|
@ -15,10 +15,17 @@
|
||||
package clair
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//var client = NewClient()
|
||||
|
||||
// ParseClairSev parse the severity of clair to Harbor's Severity type if the string is not recognized the value will be set to unknown.
|
||||
func ParseClairSev(clairSev string) models.Severity {
|
||||
sev := strings.ToLower(clairSev)
|
||||
@ -35,3 +42,54 @@ func ParseClairSev(clairSev string) models.Severity {
|
||||
return models.SevUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateScanOverview qeuries the vulnerability based on the layerName and update the record in img_scan_overview table based on digest.
|
||||
func UpdateScanOverview(digest, layerName string, l ...*log.Logger) error {
|
||||
var logger *log.Logger
|
||||
if len(l) > 1 {
|
||||
return fmt.Errorf("More than one logger specified")
|
||||
} else if len(l) == 1 {
|
||||
logger = l[0]
|
||||
} else {
|
||||
logger = log.DefaultLogger()
|
||||
}
|
||||
client := NewClient(common.DefaultClairEndpoint, logger)
|
||||
res, err := client.GetResult(layerName)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get result from Clair, error: %v", err)
|
||||
return err
|
||||
}
|
||||
vulnMap := make(map[models.Severity]int)
|
||||
features := res.Layer.Features
|
||||
totalComponents := len(features)
|
||||
logger.Infof("total features: %d", totalComponents)
|
||||
var temp models.Severity
|
||||
for _, f := range features {
|
||||
sev := models.SevNone
|
||||
for _, v := range f.Vulnerabilities {
|
||||
temp = ParseClairSev(v.Severity)
|
||||
if temp > sev {
|
||||
sev = temp
|
||||
}
|
||||
}
|
||||
logger.Infof("Feature: %s, Severity: %d", f.Name, sev)
|
||||
vulnMap[sev]++
|
||||
}
|
||||
overallSev := models.SevNone
|
||||
compSummary := []*models.ComponentsOverviewEntry{}
|
||||
for k, v := range vulnMap {
|
||||
if k > overallSev {
|
||||
overallSev = k
|
||||
}
|
||||
entry := &models.ComponentsOverviewEntry{
|
||||
Sev: int(k),
|
||||
Count: v,
|
||||
}
|
||||
compSummary = append(compSummary, entry)
|
||||
}
|
||||
compOverview := &models.ComponentsOverview{
|
||||
Total: totalComponents,
|
||||
Summary: compSummary,
|
||||
}
|
||||
return dao.UpdateImgScanOverview(digest, layerName, overallSev, compOverview)
|
||||
}
|
||||
|
@ -18,13 +18,13 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Error : if response is returned but the status code is not 200, an Error instance will be returned
|
||||
type Error struct {
|
||||
// HTTPError : if response is returned but the status code is not 200, an Error instance will be returned
|
||||
type HTTPError struct {
|
||||
StatusCode int
|
||||
Detail string
|
||||
}
|
||||
|
||||
// Error returns the details as string
|
||||
func (e *Error) Error() string {
|
||||
func (e *HTTPError) Error() string {
|
||||
return fmt.Sprintf("%d %s", e.StatusCode, e.Detail)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
err := &Error{
|
||||
err := &HTTPError{
|
||||
StatusCode: 404,
|
||||
Detail: "not found",
|
||||
}
|
||||
|
@ -48,6 +48,9 @@ func GetSystemLdapConf() (models.LdapConf, error) {
|
||||
}
|
||||
|
||||
ldap, err := config.LDAP()
|
||||
if err != nil {
|
||||
return ldapConfs, err
|
||||
}
|
||||
|
||||
ldapConfs.LdapURL = ldap.URL
|
||||
ldapConfs.LdapSearchDn = ldap.SearchDN
|
||||
|
@ -164,6 +164,9 @@ func (s *standardTokenAuthorizer) generateToken(realm, service string, scopes []
|
||||
realm = s.tokenURL(realm)
|
||||
tk, err := getToken(s.client, s.credential, realm,
|
||||
service, scopes)
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
|
||||
if len(tk.IssuedAt) == 0 {
|
||||
return tk.Token, tk.ExpiresIn, nil, nil
|
||||
|
@ -78,7 +78,7 @@ func getToken(client *http.Client, credential Credential, realm, service string,
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, ®istry_error.Error{
|
||||
return nil, ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(data),
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ func (r *Registry) Catalog() ([]string, error) {
|
||||
suffix = ""
|
||||
}
|
||||
} else {
|
||||
return repos, ®istry_error.Error{
|
||||
return repos, ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -157,7 +157,7 @@ func (r *Registry) Ping() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ®istry_error.Error{
|
||||
return ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ func NewRepositoryWithModifiers(name, endpoint string, insecure bool, modifiers
|
||||
|
||||
func parseError(err error) error {
|
||||
if urlErr, ok := err.(*url.Error); ok {
|
||||
if regErr, ok := urlErr.Err.(*registry_error.Error); ok {
|
||||
if regErr, ok := urlErr.Err.(*registry_error.HTTPError); ok {
|
||||
return regErr
|
||||
}
|
||||
}
|
||||
@ -120,7 +120,7 @@ func (r *Repository) ListTag() ([]string, error) {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
return tags, ®istry_error.Error{
|
||||
return tags, ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -160,7 +160,7 @@ func (r *Repository) ManifestExist(reference string) (digest string, exist bool,
|
||||
return
|
||||
}
|
||||
|
||||
err = ®istry_error.Error{
|
||||
err = ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -197,7 +197,7 @@ func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) (
|
||||
return
|
||||
}
|
||||
|
||||
err = ®istry_error.Error{
|
||||
err = ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -232,7 +232,7 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) (
|
||||
return
|
||||
}
|
||||
|
||||
err = ®istry_error.Error{
|
||||
err = ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -263,7 +263,7 @@ func (r *Repository) DeleteManifest(digest string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ®istry_error.Error{
|
||||
return ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -277,7 +277,7 @@ func (r *Repository) DeleteTag(tag string) error {
|
||||
}
|
||||
|
||||
if !exist {
|
||||
return ®istry_error.Error{
|
||||
return ®istry_error.HTTPError{
|
||||
StatusCode: http.StatusNotFound,
|
||||
}
|
||||
}
|
||||
@ -312,7 +312,7 @@ func (r *Repository) BlobExist(digest string) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, ®istry_error.Error{
|
||||
return false, ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -348,7 +348,7 @@ func (r *Repository) PullBlob(digest string) (size int64, data io.ReadCloser, er
|
||||
return
|
||||
}
|
||||
|
||||
err = ®istry_error.Error{
|
||||
err = ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -379,7 +379,7 @@ func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID strin
|
||||
return
|
||||
}
|
||||
|
||||
err = ®istry_error.Error{
|
||||
err = ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -409,7 +409,7 @@ func (r *Repository) monolithicBlobUpload(location, digest string, size int64, d
|
||||
return err
|
||||
}
|
||||
|
||||
return ®istry_error.Error{
|
||||
return ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
@ -447,7 +447,7 @@ func (r *Repository) DeleteBlob(digest string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ®istry_error.Error{
|
||||
return ®istry_error.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
|
@ -396,10 +396,10 @@ func TestListTag(t *testing.T) {
|
||||
|
||||
func TestParseError(t *testing.T) {
|
||||
err := &url.Error{
|
||||
Err: ®istry_error.Error{},
|
||||
Err: ®istry_error.HTTPError{},
|
||||
}
|
||||
e := parseError(err)
|
||||
if _, ok := e.(*registry_error.Error); !ok {
|
||||
if _, ok := e.(*registry_error.HTTPError); !ok {
|
||||
t.Errorf("error type does not match registry error")
|
||||
}
|
||||
}
|
||||
|
80
src/common/utils/timemarker.go
Normal file
80
src/common/utils/timemarker.go
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
scanAllMarker *TimeMarker
|
||||
scanOverviewMarker = &TimeMarker{
|
||||
interval: 15 * time.Second,
|
||||
}
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
//TimeMarker is used to control an action not to be taken frequently within the interval
|
||||
type TimeMarker struct {
|
||||
sync.RWMutex
|
||||
next time.Time
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
//Mark tries to mark a future time, which is after the duration of interval from the time it's called.
|
||||
func (t *TimeMarker) Mark() {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
t.next = time.Now().Add(t.interval)
|
||||
}
|
||||
|
||||
//Check returns true if the current time is after the mark by this marker, and the caction the mark guards and be taken.
|
||||
func (t *TimeMarker) Check() bool {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
return time.Now().After(t.next)
|
||||
}
|
||||
|
||||
//Next returns the time of the next mark.
|
||||
func (t *TimeMarker) Next() time.Time {
|
||||
t.RLock()
|
||||
defer t.RUnlock()
|
||||
return t.next
|
||||
}
|
||||
|
||||
//ScanAllMarker ...
|
||||
func ScanAllMarker() *TimeMarker {
|
||||
once.Do(func() {
|
||||
a := os.Getenv("HARBOR_SCAN_ALL_INTERVAL")
|
||||
if m, err := strconv.Atoi(a); err == nil {
|
||||
scanAllMarker = &TimeMarker{
|
||||
interval: time.Duration(m) * time.Minute,
|
||||
}
|
||||
} else {
|
||||
scanAllMarker = &TimeMarker{
|
||||
interval: 30 * time.Minute,
|
||||
}
|
||||
}
|
||||
})
|
||||
return scanAllMarker
|
||||
}
|
||||
|
||||
//ScanOverviewMarker ...
|
||||
func ScanOverviewMarker() *TimeMarker {
|
||||
return scanOverviewMarker
|
||||
}
|
49
src/common/utils/timemarker_test.go
Normal file
49
src/common/utils/timemarker_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTimeMarker(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
m := &TimeMarker{
|
||||
interval: 1 * time.Second,
|
||||
}
|
||||
r1 := m.Check()
|
||||
assert.True(r1)
|
||||
m.Mark()
|
||||
r2 := m.Check()
|
||||
assert.False(r2)
|
||||
t.Log("Sleep for 2 seconds...")
|
||||
time.Sleep(2 * time.Second)
|
||||
r3 := m.Check()
|
||||
assert.True(r3)
|
||||
}
|
||||
|
||||
func TestScanMarkers(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
os.Setenv("HARBOR_SCAN_ALL_INTERVAL", "5")
|
||||
sm := ScanAllMarker()
|
||||
d := sm.Next().Sub(time.Now())
|
||||
assert.True(d <= 5*time.Minute)
|
||||
som := ScanOverviewMarker()
|
||||
d = som.Next().Sub(time.Now())
|
||||
assert.True(d <= 15*time.Second)
|
||||
}
|
@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -127,6 +128,48 @@ func ParseTimeStamp(timestamp string) (*time.Time, error) {
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
//ConvertMapToStruct is used to fill the specified struct with map.
|
||||
func ConvertMapToStruct(object interface{}, valuesInMap map[string]interface{}) error {
|
||||
if object == nil {
|
||||
return fmt.Errorf("nil struct is not supported")
|
||||
}
|
||||
|
||||
if reflect.TypeOf(object).Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("object should be referred by pointer")
|
||||
}
|
||||
|
||||
for k, v := range valuesInMap {
|
||||
if err := setField(object, k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setField(object interface{}, field string, value interface{}) error {
|
||||
structValue := reflect.ValueOf(object).Elem()
|
||||
|
||||
structFieldValue := structValue.FieldByName(field)
|
||||
if !structFieldValue.IsValid() {
|
||||
return fmt.Errorf("No such field: %s in obj", field)
|
||||
}
|
||||
|
||||
if !structFieldValue.CanSet() {
|
||||
return fmt.Errorf("Cannot set value for field %s", field)
|
||||
}
|
||||
|
||||
structFieldType := structFieldValue.Type()
|
||||
val := reflect.ValueOf(value)
|
||||
if structFieldType != val.Type() {
|
||||
return errors.New("Provided value type didn't match object field type")
|
||||
}
|
||||
|
||||
structFieldValue.Set(val)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseProjectIDOrName parses value to ID(int64) or name(string)
|
||||
func ParseProjectIDOrName(value interface{}) (int64, string, error) {
|
||||
if value == nil {
|
||||
|
@ -243,3 +243,23 @@ func TestParseHarborIDOrName(t *testing.T) {
|
||||
assert.Equal(t, int64(0), id)
|
||||
assert.Equal(t, "project", name)
|
||||
}
|
||||
|
||||
type testingStruct struct {
|
||||
Name string
|
||||
Count int
|
||||
}
|
||||
|
||||
func TestConvertMapToStruct(t *testing.T) {
|
||||
dataMap := make(map[string]interface{})
|
||||
dataMap["Name"] = "testing"
|
||||
dataMap["Count"] = 100
|
||||
|
||||
obj := &testingStruct{}
|
||||
if err := ConvertMapToStruct(obj, dataMap); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if obj.Name != "testing" || obj.Count != 100 {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,8 +33,7 @@ type ImageScanJob struct {
|
||||
|
||||
// Prepare ...
|
||||
func (isj *ImageScanJob) Prepare() {
|
||||
//TODO Uncomment to enable security check.
|
||||
// isj.authenticate()
|
||||
isj.authenticate()
|
||||
}
|
||||
|
||||
// Post creates a scanner job and hand it to statemachine.
|
||||
|
@ -170,5 +170,5 @@ func InternalTokenServiceEndpoint() string {
|
||||
|
||||
// ClairEndpoint returns the end point of clair instance, by default it's the one deployed within Harbor.
|
||||
func ClairEndpoint() string {
|
||||
return "http://clair:6060"
|
||||
return common.DefaultClairEndpoint
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ package scan
|
||||
import (
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/clair"
|
||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||
@ -135,44 +134,9 @@ func (sh *SummarizeHandler) Enter() (string, error) {
|
||||
logger.Infof("Entered summarize handler")
|
||||
layerName := sh.Context.layers[len(sh.Context.layers)-1].Name
|
||||
logger.Infof("Top layer's name: %s, will use it to get the vulnerability result of image", layerName)
|
||||
res, err := sh.Context.clairClient.GetResult(layerName)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to get result from Clair, error: %v", err)
|
||||
return "", err
|
||||
if err := clair.UpdateScanOverview(sh.Context.Digest, layerName); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
vulnMap := make(map[models.Severity]int)
|
||||
features := res.Layer.Features
|
||||
totalComponents := len(features)
|
||||
logger.Infof("total features: %d", totalComponents)
|
||||
var temp models.Severity
|
||||
for _, f := range features {
|
||||
sev := models.SevNone
|
||||
for _, v := range f.Vulnerabilities {
|
||||
temp = clair.ParseClairSev(v.Severity)
|
||||
if temp > sev {
|
||||
sev = temp
|
||||
}
|
||||
}
|
||||
logger.Infof("Feature: %s, Severity: %d", f.Name, sev)
|
||||
vulnMap[sev]++
|
||||
}
|
||||
overallSev := models.SevNone
|
||||
compSummary := []*models.ComponentsOverviewEntry{}
|
||||
for k, v := range vulnMap {
|
||||
if k > overallSev {
|
||||
overallSev = k
|
||||
}
|
||||
entry := &models.ComponentsOverviewEntry{
|
||||
Sev: int(k),
|
||||
Count: v,
|
||||
}
|
||||
compSummary = append(compSummary, entry)
|
||||
}
|
||||
compOverview := &models.ComponentsOverview{
|
||||
Total: totalComponents,
|
||||
Summary: compSummary,
|
||||
}
|
||||
err = dao.UpdateImgScanOverview(sh.Context.Digest, layerName, overallSev, compOverview)
|
||||
return models.JobFinished, nil
|
||||
}
|
||||
|
||||
|
@ -190,6 +190,9 @@ func (c *ConfigAPI) Put() {
|
||||
log.Errorf("failed to load configurations: %v", err)
|
||||
c.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
//Everything is ok, detect the configurations to confirm if the option we are caring is changed.
|
||||
watchConfigChanges(cfg)
|
||||
}
|
||||
|
||||
// Reset system configurations
|
||||
@ -293,7 +296,7 @@ func validateCfg(c map[string]interface{}) (bool, error) {
|
||||
scope != common.LDAPScopeBase &&
|
||||
scope != common.LDAPScopeOnelevel &&
|
||||
scope != common.LDAPScopeSubtree {
|
||||
return false, fmt.Errorf("invalid %s, should be %s, %s or %s",
|
||||
return false, fmt.Errorf("invalid %s, should be %d, %d or %d",
|
||||
common.LDAPScope,
|
||||
common.LDAPScopeBase,
|
||||
common.LDAPScopeOnelevel,
|
||||
|
@ -64,11 +64,12 @@ func (pma *ProjectMemberAPI) Prepare() {
|
||||
} else {
|
||||
text += fmt.Sprintf("%d", pid)
|
||||
}
|
||||
pma.HandleBadRequest(text)
|
||||
return
|
||||
}
|
||||
project, err := pma.ProjectMgr.Get(pid)
|
||||
if err != nil {
|
||||
pma.HandleInternalServerError(
|
||||
fmt.Sprintf("failed to get project %d: %v", pid, err))
|
||||
pma.ParseAndHandleError(fmt.Sprintf("failed to get project %d", pid), err)
|
||||
return
|
||||
}
|
||||
if project == nil {
|
||||
@ -77,8 +78,8 @@ func (pma *ProjectMemberAPI) Prepare() {
|
||||
}
|
||||
pma.project = project
|
||||
|
||||
if pma.Ctx.Input.IsGet() && !pma.SecurityCtx.HasReadPerm(pid) ||
|
||||
!pma.SecurityCtx.HasAllPerm(pid) {
|
||||
if !(pma.Ctx.Input.IsGet() && pma.SecurityCtx.HasReadPerm(pid) ||
|
||||
pma.SecurityCtx.HasAllPerm(pid)) {
|
||||
pma.HandleForbidden(pma.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
@ -59,8 +59,7 @@ func (p *ProjectAPI) Prepare() {
|
||||
|
||||
project, err := p.ProjectMgr.Get(id)
|
||||
if err != nil {
|
||||
p.HandleInternalServerError(fmt.Sprintf("failed to get project %d: %v",
|
||||
id, err))
|
||||
p.ParseAndHandleError(fmt.Sprintf("failed to get project %d", id), err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -107,8 +106,8 @@ func (p *ProjectAPI) Post() {
|
||||
|
||||
exist, err := p.ProjectMgr.Exist(pro.Name)
|
||||
if err != nil {
|
||||
p.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %s: %v",
|
||||
pro.Name, err))
|
||||
p.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %s",
|
||||
pro.Name), err)
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
@ -126,12 +125,12 @@ func (p *ProjectAPI) Post() {
|
||||
AutomaticallyScanImagesOnPush: pro.AutomaticallyScanImagesOnPush,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to add project, error: %v", err)
|
||||
dup, _ := regexp.MatchString(dupProjectPattern, err.Error())
|
||||
if dup {
|
||||
log.Debugf("conflict %s", pro.Name)
|
||||
p.RenderError(http.StatusConflict, "")
|
||||
} else {
|
||||
p.RenderError(http.StatusInternalServerError, "Failed to add project")
|
||||
p.ParseAndHandleError("failed to add project", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -163,8 +162,7 @@ func (p *ProjectAPI) Head() {
|
||||
|
||||
project, err := p.ProjectMgr.Get(name)
|
||||
if err != nil {
|
||||
p.HandleInternalServerError(fmt.Sprintf("failed to get project %s: %v",
|
||||
name, err))
|
||||
p.ParseAndHandleError(fmt.Sprintf("failed to get project %s", name), err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -223,8 +221,7 @@ func (p *ProjectAPI) Delete() {
|
||||
}
|
||||
|
||||
if err = p.ProjectMgr.Delete(p.project.ProjectID); err != nil {
|
||||
p.HandleInternalServerError(
|
||||
fmt.Sprintf("failed to delete project %d: %v", p.project.ProjectID, err))
|
||||
p.ParseAndHandleError(fmt.Sprintf("failed to delete project %d", p.project.ProjectID), err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -299,13 +296,13 @@ func (p *ProjectAPI) List() {
|
||||
|
||||
total, err := p.ProjectMgr.GetTotal(query, base)
|
||||
if err != nil {
|
||||
p.HandleInternalServerError(fmt.Sprintf("failed to get total of projects: %v", err))
|
||||
p.ParseAndHandleError("failed to get total of projects", err)
|
||||
return
|
||||
}
|
||||
|
||||
projects, err := p.ProjectMgr.GetAll(query, base)
|
||||
if err != nil {
|
||||
p.HandleInternalServerError(fmt.Sprintf("failed to get projects: %v", err))
|
||||
p.ParseAndHandleError("failed to get projects", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -359,8 +356,8 @@ func (p *ProjectAPI) ToggleProjectPublic() {
|
||||
&models.Project{
|
||||
Public: req.Public,
|
||||
}); err != nil {
|
||||
p.HandleInternalServerError(fmt.Sprintf("failed to update project %d: %v",
|
||||
p.project.ProjectID, err))
|
||||
p.ParseAndHandleError(fmt.Sprintf("failed to update project %d",
|
||||
p.project.ProjectID), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -86,8 +86,8 @@ func (pa *RepPolicyAPI) List() {
|
||||
for _, policy := range policies {
|
||||
project, err := pa.ProjectMgr.Get(policy.ProjectID)
|
||||
if err != nil {
|
||||
pa.HandleInternalServerError(fmt.Sprintf(
|
||||
"failed to get project %d: %v", policy.ProjectID, err))
|
||||
pa.ParseAndHandleError(fmt.Sprintf(
|
||||
"failed to get project %d", policy.ProjectID), err)
|
||||
return
|
||||
}
|
||||
if project != nil {
|
||||
@ -118,8 +118,8 @@ func (pa *RepPolicyAPI) Post() {
|
||||
|
||||
project, err := pa.ProjectMgr.Get(policy.ProjectID)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get project %d: %v", policy.ProjectID, err)
|
||||
pa.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
pa.ParseAndHandleError(fmt.Sprintf("failed to get project %d", policy.ProjectID), err)
|
||||
return
|
||||
}
|
||||
|
||||
if project == nil {
|
||||
|
@ -84,8 +84,8 @@ func (ra *RepositoryAPI) Get() {
|
||||
|
||||
exist, err := ra.ProjectMgr.Exist(projectID)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %d: %v",
|
||||
projectID, err))
|
||||
ra.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %d",
|
||||
projectID), err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -169,8 +169,8 @@ func (ra *RepositoryAPI) Delete() {
|
||||
projectName, _ := utils.ParseRepository(repoName)
|
||||
project, err := ra.ProjectMgr.Get(projectName)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to get the project %s: %v",
|
||||
projectName, err))
|
||||
ra.ParseAndHandleError(fmt.Sprintf("failed to get the project %s",
|
||||
projectName), err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -200,7 +200,7 @@ func (ra *RepositoryAPI) Delete() {
|
||||
if len(tag) == 0 {
|
||||
tagList, err := rc.ListTag()
|
||||
if err != nil {
|
||||
if regErr, ok := err.(*registry_error.Error); ok {
|
||||
if regErr, ok := err.(*registry_error.HTTPError); ok {
|
||||
ra.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
@ -242,7 +242,7 @@ func (ra *RepositoryAPI) Delete() {
|
||||
|
||||
for _, t := range tags {
|
||||
if err = rc.DeleteTag(t); err != nil {
|
||||
if regErr, ok := err.(*registry_error.Error); ok {
|
||||
if regErr, ok := err.(*registry_error.HTTPError); ok {
|
||||
if regErr.StatusCode == http.StatusNotFound {
|
||||
continue
|
||||
}
|
||||
@ -335,8 +335,8 @@ func (ra *RepositoryAPI) GetTags() {
|
||||
projectName, _ := utils.ParseRepository(repoName)
|
||||
exist, err := ra.ProjectMgr.Exist(projectName)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %s: %v",
|
||||
projectName, err))
|
||||
ra.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %s",
|
||||
projectName), err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -475,8 +475,8 @@ func (ra *RepositoryAPI) GetManifests() {
|
||||
projectName, _ := utils.ParseRepository(repoName)
|
||||
exist, err := ra.ProjectMgr.Exist(projectName)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %s: %v",
|
||||
projectName, err))
|
||||
ra.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %s",
|
||||
projectName), err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -503,7 +503,7 @@ func (ra *RepositoryAPI) GetManifests() {
|
||||
|
||||
manifest, err := getManifest(rc, tag, version)
|
||||
if err != nil {
|
||||
if regErr, ok := err.(*registry_error.Error); ok {
|
||||
if regErr, ok := err.(*registry_error.HTTPError); ok {
|
||||
ra.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
@ -577,7 +577,7 @@ func (ra *RepositoryAPI) GetTopRepos() {
|
||||
projectIDs := []int64{}
|
||||
projects, err := ra.ProjectMgr.GetPublic()
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to get public projects: %v", err))
|
||||
ra.ParseAndHandleError("failed to get public projects", err)
|
||||
return
|
||||
}
|
||||
if ra.SecurityCtx.IsAuthenticated() {
|
||||
@ -617,8 +617,8 @@ func (ra *RepositoryAPI) GetSignatures() {
|
||||
projectName, _ := utils.ParseRepository(repoName)
|
||||
exist, err := ra.ProjectMgr.Exist(projectName)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %s: %v",
|
||||
projectName, err))
|
||||
ra.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %s",
|
||||
projectName), err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -658,8 +658,8 @@ func (ra *RepositoryAPI) ScanImage() {
|
||||
projectName, _ := utils.ParseRepository(repoName)
|
||||
exist, err := ra.ProjectMgr.Exist(projectName)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %s: %v",
|
||||
projectName, err))
|
||||
ra.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %s",
|
||||
projectName), err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
@ -745,11 +745,19 @@ func (ra *RepositoryAPI) ScanAll() {
|
||||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
|
||||
if !utils.ScanAllMarker().Check() {
|
||||
log.Warningf("There is a scan all scheduled at: %v, the request will not be processed.", utils.ScanAllMarker().Next())
|
||||
ra.RenderError(http.StatusPreconditionFailed, "Unable handle frequent scan all requests")
|
||||
return
|
||||
}
|
||||
|
||||
if err := uiutils.ScanAllImages(); err != nil {
|
||||
log.Errorf("Failed triggering scan all images, error: %v", err)
|
||||
ra.HandleInternalServerError(fmt.Sprintf("Error: %v", err))
|
||||
return
|
||||
}
|
||||
utils.ScanAllMarker().Mark()
|
||||
ra.Ctx.ResponseWriter.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
@ -776,7 +784,7 @@ func (ra *RepositoryAPI) checkExistence(repository, tag string) (bool, string, e
|
||||
project, _ := utils.ParseRepository(repository)
|
||||
exist, err := ra.ProjectMgr.Exist(project)
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("failed to check the existence of project %s: %v", project, err)
|
||||
return false, "", err
|
||||
}
|
||||
if !exist {
|
||||
log.Errorf("project %s not found", project)
|
||||
|
@ -51,15 +51,13 @@ func (s *SearchAPI) Get() {
|
||||
if isSysAdmin {
|
||||
projects, err = s.ProjectMgr.GetAll(nil)
|
||||
if err != nil {
|
||||
s.HandleInternalServerError(fmt.Sprintf(
|
||||
"failed to get projects: %v", err))
|
||||
s.ParseAndHandleError("failed to get projects", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
projects, err = s.ProjectMgr.GetPublic()
|
||||
if err != nil {
|
||||
s.HandleInternalServerError(fmt.Sprintf(
|
||||
"failed to get projects: %v", err))
|
||||
s.ParseAndHandleError("failed to get projects", err)
|
||||
return
|
||||
}
|
||||
if isAuthenticated {
|
||||
|
@ -59,8 +59,7 @@ func (s *StatisticAPI) Get() {
|
||||
statistic := map[string]int64{}
|
||||
pubProjs, err := s.ProjectMgr.GetPublic()
|
||||
if err != nil {
|
||||
s.HandleInternalServerError(fmt.Sprintf(
|
||||
"failed to get public projects: %v", err))
|
||||
s.ParseAndHandleError("failed to get public projects", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -78,7 +77,7 @@ func (s *StatisticAPI) Get() {
|
||||
statistic[PubRC] = n
|
||||
|
||||
if s.SecurityCtx.IsSysAdmin() {
|
||||
n, err := dao.GetTotalOfProjects(nil)
|
||||
n, err := s.ProjectMgr.GetTotal(nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get total of projects: %v", err)
|
||||
s.CustomAbort(http.StatusInternalServerError, "")
|
||||
@ -102,8 +101,8 @@ func (s *StatisticAPI) Get() {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.HandleInternalServerError(fmt.Sprintf(
|
||||
"failed to get projects of user %s: %v", s.username, err))
|
||||
s.ParseAndHandleError(fmt.Sprintf(
|
||||
"failed to get projects of user %s", s.username), err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,12 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
clairdao "github.com/vmware/harbor/src/common/dao/clair"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
@ -46,16 +50,17 @@ type Storage struct {
|
||||
|
||||
//GeneralInfo wraps common systeminfo for anonymous request
|
||||
type GeneralInfo struct {
|
||||
WithNotary bool `json:"with_notary"`
|
||||
WithClair bool `json:"with_clair"`
|
||||
WithAdmiral bool `json:"with_admiral"`
|
||||
AdmiralEndpoint string `json:"admiral_endpoint"`
|
||||
AuthMode string `json:"auth_mode"`
|
||||
RegistryURL string `json:"registry_url"`
|
||||
ProjectCreationRestrict string `json:"project_creation_restriction"`
|
||||
SelfRegistration bool `json:"self_registration"`
|
||||
HasCARoot bool `json:"has_ca_root"`
|
||||
HarborVersion string `json:"harbor_version"`
|
||||
WithNotary bool `json:"with_notary"`
|
||||
WithClair bool `json:"with_clair"`
|
||||
WithAdmiral bool `json:"with_admiral"`
|
||||
AdmiralEndpoint string `json:"admiral_endpoint"`
|
||||
AuthMode string `json:"auth_mode"`
|
||||
RegistryURL string `json:"registry_url"`
|
||||
ProjectCreationRestrict string `json:"project_creation_restriction"`
|
||||
SelfRegistration bool `json:"self_registration"`
|
||||
HasCARoot bool `json:"has_ca_root"`
|
||||
HarborVersion string `json:"harbor_version"`
|
||||
ClairVulnStatus *models.ClairVulnerabilityStatus `json:"clair_vulnerability_status,omitempty"`
|
||||
}
|
||||
|
||||
// validate for validating user if an admin.
|
||||
@ -134,11 +139,14 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
|
||||
HasCARoot: caStatErr == nil,
|
||||
HarborVersion: harborVersion,
|
||||
}
|
||||
if info.WithClair {
|
||||
info.ClairVulnStatus = getClairVulnStatus()
|
||||
}
|
||||
sia.Data["json"] = info
|
||||
sia.ServeJSON()
|
||||
}
|
||||
|
||||
// GetVersion gets harbor version.
|
||||
// getVersion gets harbor version.
|
||||
func (sia *SystemInfoAPI) getVersion() string {
|
||||
version, err := ioutil.ReadFile(harborVersionFile)
|
||||
if err != nil {
|
||||
@ -147,3 +155,37 @@ func (sia *SystemInfoAPI) getVersion() string {
|
||||
}
|
||||
return string(version[:])
|
||||
}
|
||||
|
||||
func getClairVulnStatus() *models.ClairVulnerabilityStatus {
|
||||
res := &models.ClairVulnerabilityStatus{}
|
||||
last, err := clairdao.GetLastUpdate()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get last update from Clair DB, error: %v", err)
|
||||
res.OverallUTC = 0
|
||||
} else {
|
||||
res.OverallUTC = last
|
||||
log.Debugf("Clair vuln DB last update: %d", last)
|
||||
}
|
||||
l, err := dao.ListClairVulnTimestamps()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to list Clair vulnerability timestamps, error:%v", err)
|
||||
return res
|
||||
}
|
||||
m := make(map[string]time.Time)
|
||||
for _, e := range l {
|
||||
ns := strings.Split(e.Namespace, ":")
|
||||
if ts, ok := m[ns[0]]; !ok || ts.Before(e.LastUpdate) {
|
||||
m[ns[0]] = e.LastUpdate
|
||||
}
|
||||
}
|
||||
details := []models.ClairNamespaceTimestamp{}
|
||||
for k, v := range m {
|
||||
e := models.ClairNamespaceTimestamp{
|
||||
Namespace: k,
|
||||
Timestamp: v.UTC().Unix(),
|
||||
}
|
||||
details = append(details, e)
|
||||
}
|
||||
res.Details = details
|
||||
return res
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ func (t *TargetAPI) ping(endpoint, username, password string) {
|
||||
}
|
||||
|
||||
if err = registry.Ping(); err != nil {
|
||||
if regErr, ok := err.(*registry_error.Error); ok {
|
||||
if regErr, ok := err.(*registry_error.HTTPError); ok {
|
||||
t.CustomAbort(regErr.StatusCode, regErr.Detail)
|
||||
}
|
||||
|
||||
|
@ -17,14 +17,17 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/notifier"
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/clair"
|
||||
registry_error "github.com/vmware/harbor/src/common/utils/error"
|
||||
@ -440,7 +443,7 @@ func getReposByProject(name string, keyword ...string) ([]string, error) {
|
||||
func repositoryExist(name string, client *registry.Repository) (bool, error) {
|
||||
tags, err := client.ListTag()
|
||||
if err != nil {
|
||||
if regErr, ok := err.(*registry_error.Error); ok && regErr.StatusCode == http.StatusNotFound {
|
||||
if regErr, ok := err.(*registry_error.HTTPError); ok && regErr.StatusCode == http.StatusNotFound {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
@ -496,3 +499,35 @@ func transformVulnerabilities(layerWithVuln *models.ClairLayerEnvelope) []*model
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
//Watch the configuration changes.
|
||||
func watchConfigChanges(cfg map[string]interface{}) error {
|
||||
if cfg == nil {
|
||||
return errors.New("Empty configurations")
|
||||
}
|
||||
|
||||
//Currently only watch the scan all policy change.
|
||||
if v, ok := cfg[notifier.ScanAllPolicyTopic]; ok {
|
||||
if reflect.TypeOf(v).Kind() == reflect.Map {
|
||||
policyCfg := &models.ScanAllPolicy{}
|
||||
if err := utils.ConvertMapToStruct(policyCfg, v.(map[string]interface{})); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
policyNotification := notifier.ScanPolicyNotification{
|
||||
Type: policyCfg.Type,
|
||||
DailyTime: 0,
|
||||
}
|
||||
|
||||
if t, yes := policyCfg.Parm["daily_time"]; yes {
|
||||
if reflect.TypeOf(t).Kind() == reflect.Int {
|
||||
policyNotification.DailyTime = (int64)(t.(int))
|
||||
}
|
||||
}
|
||||
|
||||
return notifier.Publish(notifier.ScanAllPolicyTopic, policyNotification)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ func Reset() error {
|
||||
return mg.Reset()
|
||||
}
|
||||
|
||||
// Upload uploads all system configutations to admin server
|
||||
// Upload uploads all system configurations to admin server
|
||||
func Upload(cfg map[string]interface{}) error {
|
||||
return mg.Upload(cfg)
|
||||
}
|
||||
@ -355,7 +355,7 @@ func WithClair() bool {
|
||||
|
||||
// ClairEndpoint returns the end point of clair instance, by default it's the one deployed within Harbor.
|
||||
func ClairEndpoint() string {
|
||||
return "http://clair:6060"
|
||||
return common.DefaultClairEndpoint
|
||||
}
|
||||
|
||||
// AdmiralEndpoint returns the URL of admiral, if Harbor is not deployed with admiral it should return an empty string.
|
||||
|
@ -17,6 +17,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
@ -26,6 +27,8 @@ import (
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/notifier"
|
||||
"github.com/vmware/harbor/src/common/scheduler"
|
||||
"github.com/vmware/harbor/src/ui/api"
|
||||
_ "github.com/vmware/harbor/src/ui/auth/db"
|
||||
_ "github.com/vmware/harbor/src/ui/auth/ldap"
|
||||
@ -85,10 +88,14 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get database configuration: %v", err)
|
||||
}
|
||||
|
||||
if err := dao.InitDatabase(database); err != nil {
|
||||
log.Fatalf("failed to initialize database: %v", err)
|
||||
}
|
||||
if config.WithClair() {
|
||||
if err := dao.InitClairDB(); err != nil {
|
||||
log.Fatalf("failed to initialize clair database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
password, err := config.InitialAdminPassword()
|
||||
if err != nil {
|
||||
@ -98,6 +105,26 @@ func main() {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
//Enable the policy scheduler here.
|
||||
scheduler.DefaultScheduler.Start()
|
||||
|
||||
//Subscribe the policy change topic.
|
||||
notifier.Subscribe(notifier.ScanAllPolicyTopic, ¬ifier.ScanPolicyNotificationHandler{})
|
||||
|
||||
//Get policy configuration.
|
||||
scanAllPolicy := config.ScanAllPolicy()
|
||||
if scanAllPolicy.Type == notifier.PolicyTypeDaily {
|
||||
dailyTime := 0
|
||||
if t, ok := scanAllPolicy.Parm["daily_time"]; ok {
|
||||
if reflect.TypeOf(t).Kind() == reflect.Int {
|
||||
dailyTime = t.(int)
|
||||
}
|
||||
}
|
||||
|
||||
//Send notification to handle first policy change.
|
||||
notifier.Publish(notifier.ScanAllPolicyTopic, notifier.ScanPolicyNotification{Type: scanAllPolicy.Type, DailyTime: (int64)(dailyTime)})
|
||||
}
|
||||
|
||||
filter.Init()
|
||||
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
|
||||
|
||||
|
@ -412,7 +412,7 @@ func (p *ProjectManager) send(method, path string, body io.Reader) ([]byte, erro
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, &er.Error{
|
||||
return nil, &er.HTTPError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Detail: string(b),
|
||||
}
|
||||
|
@ -181,3 +181,9 @@ func TestCopyResp(t *testing.T) {
|
||||
assert.Equal(418, rec2.Result().StatusCode)
|
||||
assert.Equal("mytest", rec2.Header().Get("X-Test"))
|
||||
}
|
||||
|
||||
func TestMarshalError(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
js := marshalError("Not Found", 404)
|
||||
assert.Equal("{\"code\":404,\"message\":\"Not Found\",\"details\":\"Not Found\"}", js)
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils/clair"
|
||||
@ -126,7 +128,7 @@ func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if flag {
|
||||
components := strings.SplitN(repository, "/", 2)
|
||||
if len(components) < 2 {
|
||||
http.Error(rw, fmt.Sprintf("Bad repository name: %s", repository), http.StatusBadRequest)
|
||||
http.Error(rw, marshalError(fmt.Sprintf("Bad repository name: %s", repository), http.StatusInternalServerError), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rec = httptest.NewRecorder()
|
||||
@ -166,12 +168,12 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
|
||||
}
|
||||
match, err := matchNotaryDigest(img)
|
||||
if err != nil {
|
||||
http.Error(rw, "Failed in communication with Notary please check the log", http.StatusInternalServerError)
|
||||
http.Error(rw, marshalError("Failed in communication with Notary please check the log", http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !match {
|
||||
log.Debugf("digest mismatch, failing the response.")
|
||||
http.Error(rw, "The image is not signed in Notary.", http.StatusPreconditionFailed)
|
||||
http.Error(rw, marshalError("The image is not signed in Notary.", http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
cth.next.ServeHTTP(rw, req)
|
||||
@ -196,18 +198,18 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
||||
overview, err := dao.GetImgScanOverview(img.digest)
|
||||
if err != nil {
|
||||
log.Errorf("failed to get ImgScanOverview with repo: %s, tag: %s, digest: %s. Error: %v", img.repository, img.tag, img.digest, err)
|
||||
http.Error(rw, "Failed to get ImgScanOverview.", http.StatusPreconditionFailed)
|
||||
http.Error(rw, marshalError("Failed to get ImgScanOverview.", http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
if overview == nil {
|
||||
log.Debugf("cannot get the image scan overview info, failing the response.")
|
||||
http.Error(rw, "Cannot get the image scan overview info.", http.StatusPreconditionFailed)
|
||||
http.Error(rw, marshalError("Cannot get the image scan overview info.", http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
imageSev := overview.Sev
|
||||
if imageSev > int(projectVulnerableSeverity) {
|
||||
log.Debugf("the image severity is higher then project setting, failing the response.")
|
||||
http.Error(rw, "The image scan result doesn't pass the project setting.", http.StatusPreconditionFailed)
|
||||
http.Error(rw, marshalError("The image scan result doesn't pass the project setting.", http.StatusPreconditionFailed), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
vh.next.ServeHTTP(rw, req)
|
||||
@ -253,3 +255,24 @@ func copyResp(rec *httptest.ResponseRecorder, rw http.ResponseWriter) {
|
||||
rw.WriteHeader(rec.Result().StatusCode)
|
||||
rw.Write(rec.Body.Bytes())
|
||||
}
|
||||
|
||||
func marshalError(msg string, statusCode int) string {
|
||||
je := &JSONError{
|
||||
Message: msg,
|
||||
Code: statusCode,
|
||||
Details: msg,
|
||||
}
|
||||
str, err := json.Marshal(je)
|
||||
if err != nil {
|
||||
log.Debugf("failed to marshal json error, %v", err)
|
||||
return msg
|
||||
}
|
||||
return string(str)
|
||||
}
|
||||
|
||||
// JSONError wraps a concrete Code and Message, it's readable for docker deamon.
|
||||
type JSONError struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/ui/api"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
"github.com/vmware/harbor/src/ui/controllers"
|
||||
"github.com/vmware/harbor/src/ui/service/notifications/clair"
|
||||
"github.com/vmware/harbor/src/ui/service/notifications/registry"
|
||||
@ -55,23 +56,34 @@ func initRouters() {
|
||||
beego.Router("/harbor/tags", &controllers.IndexController{})
|
||||
beego.Router("/harbor/configs", &controllers.IndexController{})
|
||||
|
||||
beego.Router("/login", &controllers.CommonController{}, "post:Login")
|
||||
beego.Router("/log_out", &controllers.CommonController{}, "get:LogOut")
|
||||
beego.Router("/reset", &controllers.CommonController{}, "post:ResetPassword")
|
||||
beego.Router("/userExists", &controllers.CommonController{}, "post:UserExists")
|
||||
beego.Router("/sendEmail", &controllers.CommonController{}, "get:SendEmail")
|
||||
// standalone
|
||||
if !config.WithAdmiral() {
|
||||
beego.Router("/login", &controllers.CommonController{}, "post:Login")
|
||||
beego.Router("/log_out", &controllers.CommonController{}, "get:LogOut")
|
||||
beego.Router("/reset", &controllers.CommonController{}, "post:ResetPassword")
|
||||
beego.Router("/userExists", &controllers.CommonController{}, "post:UserExists")
|
||||
beego.Router("/sendEmail", &controllers.CommonController{}, "get:SendEmail")
|
||||
|
||||
//API:
|
||||
//API:
|
||||
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
|
||||
beego.Router("/api/projects/", &api.ProjectAPI{}, "head:Head")
|
||||
beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{})
|
||||
beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
|
||||
|
||||
beego.Router("/api/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put")
|
||||
beego.Router("/api/users", &api.UserAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
|
||||
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
|
||||
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")
|
||||
beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "post:Search")
|
||||
beego.Router("/api/ldap/users/import", &api.LdapAPI{}, "post:ImportUser")
|
||||
beego.Router("/api/email/ping", &api.EmailAPI{}, "post:Ping")
|
||||
}
|
||||
|
||||
// API
|
||||
beego.Router("/api/search", &api.SearchAPI{})
|
||||
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
|
||||
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post;head:Head")
|
||||
beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{})
|
||||
beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
|
||||
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs")
|
||||
beego.Router("/api/statistics", &api.StatisticAPI{})
|
||||
beego.Router("/api/users/:id", &api.UserAPI{}, "get:Get;delete:Delete;put:Put")
|
||||
beego.Router("/api/users", &api.UserAPI{}, "get:List;post:Post")
|
||||
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
|
||||
beego.Router("/api/internal/syncregistry", &api.InternalAPI{}, "post:SyncRegistry")
|
||||
beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get")
|
||||
beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll")
|
||||
@ -82,6 +94,7 @@ func initRouters() {
|
||||
beego.Router("/api/repositories/*/tags/:tag/vulnerability/details", &api.RepositoryAPI{}, "Get:VulnerabilityDetails")
|
||||
beego.Router("/api/repositories/*/tags/:tag/manifest", &api.RepositoryAPI{}, "get:GetManifests")
|
||||
beego.Router("/api/repositories/*/signatures", &api.RepositoryAPI{}, "get:GetSignatures")
|
||||
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
|
||||
beego.Router("/api/jobs/replication/", &api.RepJobAPI{}, "get:List")
|
||||
beego.Router("/api/jobs/replication/:id([0-9]+)", &api.RepJobAPI{})
|
||||
beego.Router("/api/jobs/replication/:id([0-9]+)/log", &api.RepJobAPI{}, "get:GetLog")
|
||||
@ -95,19 +108,14 @@ func initRouters() {
|
||||
beego.Router("/api/targets/:id([0-9]+)/policies/", &api.TargetAPI{}, "get:ListPolicies")
|
||||
beego.Router("/api/targets/ping", &api.TargetAPI{}, "post:Ping")
|
||||
beego.Router("/api/targets/:id([0-9]+)/ping", &api.TargetAPI{}, "post:PingByID")
|
||||
beego.Router("/api/users/:id/sysadmin", &api.UserAPI{}, "put:ToggleUserAdminRole")
|
||||
beego.Router("/api/repositories/top", &api.RepositoryAPI{}, "get:GetTopRepos")
|
||||
beego.Router("/api/logs", &api.LogAPI{})
|
||||
beego.Router("/api/configurations", &api.ConfigAPI{})
|
||||
beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset")
|
||||
beego.Router("/api/statistics", &api.StatisticAPI{})
|
||||
|
||||
beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo")
|
||||
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
|
||||
beego.Router("/api/systeminfo/getcert", &api.SystemInfoAPI{}, "get:GetCert")
|
||||
beego.Router("/api/ldap/ping", &api.LdapAPI{}, "post:Ping")
|
||||
beego.Router("/api/ldap/users/search", &api.LdapAPI{}, "post:Search")
|
||||
beego.Router("/api/ldap/users/import", &api.LdapAPI{}, "post:ImportUser")
|
||||
beego.Router("/api/email/ping", &api.EmailAPI{}, "post:Ping")
|
||||
|
||||
//external service that hosted on harbor process:
|
||||
beego.Router("/service/notifications", ®istry.NotificationHandler{})
|
||||
|
@ -16,11 +16,11 @@ package clair
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
"github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/clair"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/ui/api"
|
||||
@ -31,24 +31,7 @@ const (
|
||||
rescanInterval = 15 * time.Minute
|
||||
)
|
||||
|
||||
type timer struct {
|
||||
sync.Mutex
|
||||
next time.Time
|
||||
}
|
||||
|
||||
// returns true to indicate it should reshedule the "rescan" action.
|
||||
func (t *timer) needReschedule() bool {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
if time.Now().Before(t.next) {
|
||||
return false
|
||||
}
|
||||
t.next = time.Now().Add(rescanInterval)
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
rescanTimer = timer{}
|
||||
clairClient = clair.NewClient(config.ClairEndpoint(), nil)
|
||||
)
|
||||
|
||||
@ -93,13 +76,25 @@ func (h *Handler) Handle() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if rescanTimer.needReschedule() {
|
||||
if utils.ScanOverviewMarker().Check() {
|
||||
go func() {
|
||||
<-time.After(rescanInterval)
|
||||
log.Debugf("TODO: rescan or resfresh scan_overview!")
|
||||
l, err := dao.ListImgScanOverviews()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to list scan overview records, error: %v", err)
|
||||
return
|
||||
}
|
||||
for _, e := range l {
|
||||
if err := clair.UpdateScanOverview(e.Digest, e.DetailsKey); err != nil {
|
||||
log.Errorf("Failed to refresh scan overview for image: %s", e.Digest)
|
||||
} else {
|
||||
log.Debugf("Refreshed scan overview for record with digest: %s", e.Digest)
|
||||
}
|
||||
}
|
||||
}()
|
||||
utils.ScanOverviewMarker().Mark()
|
||||
} else {
|
||||
log.Debugf("There is a rescan scheduled already, skip.")
|
||||
log.Debugf("There is a rescan scheduled at %v already, skip.", utils.ScanOverviewMarker().Next())
|
||||
}
|
||||
if err := clairClient.DeleteNotification(ne.Notification.Name); err != nil {
|
||||
log.Warningf("Failed to remove notification from Clair, name: %s", ne.Notification.Name)
|
||||
|
@ -70,6 +70,10 @@ func (n *NotificationHandler) Post() {
|
||||
log.Errorf("failed to get project by name %s: %v", project, err)
|
||||
return
|
||||
}
|
||||
if pro == nil {
|
||||
log.Warningf("project %s not found", project)
|
||||
continue
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := dao.AddAccessLog(models.AccessLog{
|
||||
|
@ -80,12 +80,13 @@ func RequestAsUI(method, url string, body io.Reader, expectSC int) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
AddUISecret(req)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
AddUISecret(req)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != expectSC {
|
||||
|
@ -6,6 +6,7 @@ Wrap the following Harbor UI components into a sharable library and published as
|
||||
* Replication endpoints management view
|
||||
* Access log list view
|
||||
* Vulnerability scanning result bar chart and list view (Embedded in tag management view)
|
||||
* Registry(Harbor) related configuration options
|
||||
|
||||
The Harbor UI library is built on **[Angular ](https://angular.io/)** 4.x and **[Clarity ](https://vmware.github.io/clarity/)** 0.9.x .
|
||||
|
||||
@ -22,12 +23,20 @@ Execute the testing specs with command:
|
||||
npm run test
|
||||
```
|
||||
|
||||
Install the package
|
||||
```
|
||||
npm install harbor-ui[@version]
|
||||
```
|
||||
|
||||
## Usage
|
||||
**Add dependency to application**
|
||||
|
||||
Execute install command to add dependency to package.json
|
||||
```
|
||||
npm install harbor-ui --save
|
||||
|
||||
//OR
|
||||
npm install harbor-ui@0.2.x --save
|
||||
```
|
||||
The latest version of the library will be installed.
|
||||
|
||||
@ -113,6 +122,16 @@ This view is linked by the repository stack view only when the Clair is enabled
|
||||
```
|
||||
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="..." [repositoryId]="..."></hbr-tag-detail>
|
||||
```
|
||||
|
||||
* **Registry related configuration**
|
||||
|
||||
This component provides some options for registry(Harbor) related configurations.
|
||||
|
||||
**hasAdminRole** is an @Input property to indicate if the current logged user has administrator role.
|
||||
|
||||
```
|
||||
<hbr-registry-config [hasAdminRole]="***"></hbr-registry-config>
|
||||
```
|
||||
## Configurations
|
||||
All the related configurations are defined in the **HarborModuleConfig** interface.
|
||||
|
||||
@ -127,6 +146,7 @@ export const DefaultServiceConfig: IServiceConfig = {
|
||||
replicationRuleEndpoint: "/api/policies/replication",
|
||||
replicationJobEndpoint: "/api/jobs/replication",
|
||||
vulnerabilityScanningBaseEndpoint: "/api/repositories",
|
||||
configurationEndpoint: "/api/configurations",
|
||||
enablei18Support: false,
|
||||
defaultLang: DEFAULT_LANG, //'en-us'
|
||||
langCookieKey: DEFAULT_LANG_COOKIE_KEY, //'harbor-lang'
|
||||
@ -165,6 +185,8 @@ It supports partially overriding. For the items not overridden, default values w
|
||||
|
||||
* **vulnerabilityScanningBaseEndpoint:** The base endpoint of the service used to handle the vulnerability scanning results.Default value is "/api/repositories".
|
||||
|
||||
* **configurationEndpoint:** The base endpoint of the service used to configure registry related options. Default is "/api/configurations".
|
||||
|
||||
* **langCookieKey:** The cookie key used to store the current used language preference. Default is "harbor-lang".
|
||||
|
||||
* **supportedLangs:** Declare what languages are supported. Default is ['en-us', 'zh-cn', 'es-es'].
|
||||
@ -254,7 +276,6 @@ export class MyAccessLogService extends AccessLogService {
|
||||
* - page
|
||||
* - pageSize
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} projectId
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[])}
|
||||
@ -268,7 +289,6 @@ export class MyAccessLogService extends AccessLogService {
|
||||
/**
|
||||
* Get the recent logs.
|
||||
*
|
||||
* @abstract
|
||||
* @param {number} lines : Specify how many lines should be returned.
|
||||
* @returns {(Observable<AccessLog[]> | Promise<AccessLog[]> | AccessLog[])}
|
||||
*
|
||||
@ -295,7 +315,6 @@ export class MyEndpointService extends EndpointService {
|
||||
* Get all the endpoints.
|
||||
* Set the argument 'endpointName' to return only the endpoints match the name pattern.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} [endpointName]
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<Endpoint[]> | Endpoint[])}
|
||||
@ -309,7 +328,6 @@ export class MyEndpointService extends EndpointService {
|
||||
/**
|
||||
* Get the specified endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} endpointId
|
||||
* @returns {(Observable<Endpoint> | Endpoint)}
|
||||
*
|
||||
@ -322,7 +340,6 @@ export class MyEndpointService extends EndpointService {
|
||||
/**
|
||||
* Create new endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Endpoint} endpoint
|
||||
* @returns {(Observable<any> | any)}
|
||||
*
|
||||
@ -335,7 +352,6 @@ export class MyEndpointService extends EndpointService {
|
||||
/**
|
||||
* Update the specified endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} endpointId
|
||||
* @param {Endpoint} endpoint
|
||||
* @returns {(Observable<any> | any)}
|
||||
@ -349,7 +365,6 @@ export class MyEndpointService extends EndpointService {
|
||||
/**
|
||||
* Delete the specified endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} endpointId
|
||||
* @returns {(Observable<any> | any)}
|
||||
*
|
||||
@ -362,7 +377,6 @@ export class MyEndpointService extends EndpointService {
|
||||
/**
|
||||
* Ping the specified endpoint.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Endpoint} endpoint
|
||||
* @returns {(Observable<any> | any)}
|
||||
*
|
||||
@ -375,7 +389,6 @@ export class MyEndpointService extends EndpointService {
|
||||
/**
|
||||
* Check endpoint whether in used with specific replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {{number | string}} endpointId
|
||||
* @returns {{Observable<any> | any}}
|
||||
*/
|
||||
@ -402,7 +415,6 @@ export class MyReplicationService extends ReplicationService {
|
||||
* set the argument 'ruleName' to return the rule only match the name pattern;
|
||||
* if pagination needed, use the queryParams to add query parameters.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} [projectId]
|
||||
* @param {string} [ruleName]
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
@ -417,7 +429,6 @@ export class MyReplicationService extends ReplicationService {
|
||||
/**
|
||||
* Get the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @returns {(Observable<ReplicationRule> | Promise<ReplicationRule> | ReplicationRule)}
|
||||
*
|
||||
@ -430,7 +441,6 @@ export class MyReplicationService extends ReplicationService {
|
||||
/**
|
||||
* Create new replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {ReplicationRule} replicationRule
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
@ -443,7 +453,6 @@ export class MyReplicationService extends ReplicationService {
|
||||
/**
|
||||
* Update the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {ReplicationRule} replicationRule
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
@ -456,7 +465,6 @@ export class MyReplicationService extends ReplicationService {
|
||||
/**
|
||||
* Delete the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
@ -469,7 +477,6 @@ export class MyReplicationService extends ReplicationService {
|
||||
/**
|
||||
* Enable the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
@ -482,7 +489,6 @@ export class MyReplicationService extends ReplicationService {
|
||||
/**
|
||||
* Disable the specified replication rule.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
@ -501,7 +507,6 @@ export class MyReplicationService extends ReplicationService {
|
||||
* - page
|
||||
* - pageSize
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} ruleId
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<ReplicationJob> | Promise<ReplicationJob[]> | ReplicationJob)}
|
||||
@ -532,7 +537,6 @@ export class MyRepositoryService extends RepositoryService {
|
||||
* 'page': current page,
|
||||
* 'page_size': page size.
|
||||
*
|
||||
* @abstract
|
||||
* @param {(number | string)} projectId
|
||||
* @param {string} repositoryName
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
@ -547,7 +551,6 @@ export class MyRepositoryService extends RepositoryService {
|
||||
/**
|
||||
* DELETE the specified repository.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} repositoryName
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
@ -572,8 +575,7 @@ export class MyTagService extends TagService {
|
||||
/**
|
||||
* Get all the tags under the specified repository.
|
||||
* NOTES: If the Notary is enabled, the signatures should be included in the returned data.
|
||||
*
|
||||
* @abstract
|
||||
*
|
||||
* @param {string} repositoryName
|
||||
* @param {RequestQueryParams} [queryParams]
|
||||
* @returns {(Observable<Tag[]> | Promise<Tag[]> | Tag[])}
|
||||
@ -587,7 +589,6 @@ export class MyTagService extends TagService {
|
||||
/**
|
||||
* Delete the specified tag.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} repositoryName
|
||||
* @param {string} tag
|
||||
* @returns {(Observable<any> | any)}
|
||||
@ -614,14 +615,12 @@ HarborLibraryModule.forRoot({
|
||||
* Get the vulnerabilities scanning results for the specified tag.
|
||||
*
|
||||
* @export
|
||||
* @abstract
|
||||
* @class ScanningResultService
|
||||
*/
|
||||
export class MyScanningResultService extends ScanningResultService {
|
||||
/**
|
||||
* Get the summary of vulnerability scanning result.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary)}
|
||||
*
|
||||
@ -634,7 +633,6 @@ export class MyScanningResultService extends ScanningResultService {
|
||||
/**
|
||||
* Get the detailed vulnerabilities scanning results.
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[])}
|
||||
*
|
||||
@ -648,7 +646,6 @@ export class MyScanningResultService extends ScanningResultService {
|
||||
/**
|
||||
* Start a new vulnerability scanning
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} repoName
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
@ -672,13 +669,11 @@ HarborLibraryModule.forRoot({
|
||||
```
|
||||
/**
|
||||
* Get System information about current backend server.
|
||||
* @abstract
|
||||
* @class
|
||||
*/
|
||||
export class MySystemInfoService extends SystemInfoService {
|
||||
/**
|
||||
* Get global system information.
|
||||
* @abstract
|
||||
* @returns
|
||||
*/
|
||||
getSystemInfo(): Observable<SystemInfo> | Promise<SystemInfo> | SystemInfo {
|
||||
@ -693,3 +688,46 @@ HarborLibraryModule.forRoot({
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
* **ConfigurationService:** Get and save the registry related configuration options.
|
||||
|
||||
```
|
||||
/**
|
||||
* Service used to get and save registry-related configurations.
|
||||
*
|
||||
* @export
|
||||
* @class MyConfigurationService
|
||||
*/
|
||||
export class MyConfigurationService extends ConfigurationService{
|
||||
|
||||
/**
|
||||
* Get configurations.
|
||||
*
|
||||
|
||||
* @returns {(Observable<Configuration> | Promise<Configuration> | Configuration)}
|
||||
*
|
||||
* @memberOf ConfigurationService
|
||||
*/
|
||||
getConfigurations(): Observable<Configuration> | Promise<Configuration> | Configuration{
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configurations.
|
||||
*
|
||||
|
||||
* @returns {(Observable<Configuration> | Promise<Configuration> | Configuration)}
|
||||
*
|
||||
* @memberOf ConfigurationService
|
||||
*/
|
||||
saveConfigurations(changedConfigs: any | { [key: string]: any | any[] }): Observable<any> | Promise<any> | any{
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
HarborLibraryModule.forRoot({
|
||||
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }
|
||||
})
|
||||
...
|
||||
```
|
||||
|
44
src/ui_ng/lib/karma.conf.win.js
Normal file
44
src/ui_ng/lib/karma.conf.win.js
Normal file
@ -0,0 +1,44 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/0.13/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular/cli'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-mocha-reporter'),
|
||||
require('karma-remap-istanbul'),
|
||||
require('@angular/cli/plugins/karma')
|
||||
],
|
||||
files: [
|
||||
{pattern: './src/test.ts', watched: false}
|
||||
],
|
||||
preprocessors: {
|
||||
'./src/test.ts': ['@angular/cli']
|
||||
},
|
||||
mime: {
|
||||
'text/x-typescript': ['ts', 'tsx']
|
||||
},
|
||||
remapIstanbulReporter: {
|
||||
reports: {
|
||||
html: 'coverage',
|
||||
lcovonly: './coverage/coverage.lcov'
|
||||
}
|
||||
},
|
||||
angularCli: {
|
||||
config: './angular-cli.json',
|
||||
environment: 'dev'
|
||||
},
|
||||
reporters: config.angularCli && config.angularCli.codeCoverage
|
||||
? ['mocha', 'karma-remap-istanbul']
|
||||
: ['mocha'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: true
|
||||
});
|
||||
};
|
@ -6,7 +6,7 @@
|
||||
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
||||
"lint": "tslint \"src/**/*.ts\"",
|
||||
"test": "ng test --single-run",
|
||||
"test:once": "karma start karma.conf.js --single-run",
|
||||
"test:win": "karma start karma.conf.win.js --single-run",
|
||||
"pree2e": "webdriver-manager update",
|
||||
"e2e": "protractor",
|
||||
"cleanup": "rimraf dist",
|
||||
@ -16,7 +16,6 @@
|
||||
"minify": "uglifyjs dist/bundles/harborui.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/harborui.umd.min.js",
|
||||
"build": "npm run cleanup && npm run transpile && npm run package && npm run minify && npm run copy"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~4.1.3",
|
||||
"@angular/common": "~4.1.3",
|
||||
@ -30,9 +29,9 @@
|
||||
"@ngx-translate/core": "^6.0.0",
|
||||
"@ngx-translate/http-loader": "0.0.3",
|
||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||
"clarity-angular": "~0.9.8",
|
||||
"clarity-icons": "~0.9.8",
|
||||
"clarity-ui": "~0.9.8",
|
||||
"clarity-angular": "^0.9.8",
|
||||
"clarity-icons": "^0.9.8",
|
||||
"clarity-ui": "^0.9.8",
|
||||
"core-js": "^2.4.1",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
@ -55,6 +54,7 @@
|
||||
"jasmine-core": "2.4.1",
|
||||
"jasmine-spec-reporter": "2.5.0",
|
||||
"karma": "1.2.0",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
@ -71,4 +71,4 @@
|
||||
"uglify-js": "^2.8.22",
|
||||
"webdriver-manager": "10.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ export class Configuration {
|
||||
this.verify_remote_cert = new BoolValueItem(false, true);
|
||||
this.scan_all_policy = new ComplexValueItem({
|
||||
type: "daily",
|
||||
parameters: {
|
||||
parameter: {
|
||||
daily_time: 0
|
||||
}
|
||||
}, true);
|
||||
|
@ -0,0 +1,9 @@
|
||||
export const REGISTRY_CONFIG_STYLES: string = `
|
||||
.info-tips-icon {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.info-tips-icon:hover {
|
||||
color: #007CBB;
|
||||
}
|
||||
`;
|
@ -1,7 +1,12 @@
|
||||
export const REGISTRY_CONFIG_HTML: string = `
|
||||
<div>
|
||||
<replication-config [(replicationConfig)]="config"></replication-config>
|
||||
<system-settings [(systemSettings)]="config"></system-settings>
|
||||
<vulnerability-config [(vulnerabilityConfig)]="config"></vulnerability-config>
|
||||
<replication-config #replicationConfig [(replicationConfig)]="config" [showSubTitle]="true"></replication-config>
|
||||
<system-settings #systemSettings [(systemSettings)]="config" [showSubTitle]="true" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>
|
||||
<vulnerability-config *ngIf="withClair" #vulnerabilityConfig [(vulnerabilityConfig)]="config" [showSubTitle]="true" [clairDBStatus]="clairDB"></vulnerability-config>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="shouldDisable">{{'BUTTON.SAVE' | translate}}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="shouldDisable">{{'BUTTON.CANCEL' | translate}}</button>
|
||||
</div>
|
||||
<confirmation-dialog #cfgConfirmationDialog (confirmAction)="confirmCancel($event)"></confirmation-dialog>
|
||||
</div>
|
||||
`;
|
@ -8,13 +8,17 @@ import { ReplicationConfigComponent } from './replication/replication-config.com
|
||||
import { SystemSettingsComponent } from './system/system-settings.component';
|
||||
import { VulnerabilityConfigComponent } from './vulnerability/vulnerability-config.component';
|
||||
import { RegistryConfigComponent } from './registry-config.component';
|
||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||
|
||||
import {
|
||||
ConfigurationService,
|
||||
import {
|
||||
ConfigurationService,
|
||||
ConfigurationDefaultService,
|
||||
ScanningResultService,
|
||||
ScanningResultDefaultService
|
||||
} from '../service/index';
|
||||
ScanningResultDefaultService,
|
||||
SystemInfoService,
|
||||
SystemInfoDefaultService,
|
||||
SystemInfo
|
||||
} from '../service/index';
|
||||
import { Configuration } from './config';
|
||||
|
||||
describe('RegistryConfigComponent (inline template)', () => {
|
||||
@ -22,20 +26,34 @@ describe('RegistryConfigComponent (inline template)', () => {
|
||||
let comp: RegistryConfigComponent;
|
||||
let fixture: ComponentFixture<RegistryConfigComponent>;
|
||||
let cfgService: ConfigurationService;
|
||||
let systemInfoService: SystemInfoService;
|
||||
let spy: jasmine.Spy;
|
||||
let saveSpy: jasmine.Spy;
|
||||
let spySystemInfo: jasmine.Spy;
|
||||
let mockConfig: Configuration = new Configuration();
|
||||
mockConfig.token_expiration.value = 90;
|
||||
mockConfig.verify_remote_cert.value = true;
|
||||
mockConfig.scan_all_policy.value = {
|
||||
type: "daily",
|
||||
parameters: {
|
||||
parameter: {
|
||||
daily_time: 0
|
||||
}
|
||||
};
|
||||
let config: IServiceConfig = {
|
||||
configurationEndpoint: '/api/configurations/testing'
|
||||
};
|
||||
let mockSystemInfo: SystemInfo = {
|
||||
"with_notary": true,
|
||||
"with_admiral": false,
|
||||
"with_clair": true,
|
||||
"admiral_endpoint": "NA",
|
||||
"auth_mode": "db_auth",
|
||||
"registry_url": "10.112.122.56",
|
||||
"project_creation_restriction": "everyone",
|
||||
"self_registration": true,
|
||||
"has_ca_root": true,
|
||||
"harbor_version": "v1.1.1-rc1-160-g565110d"
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@ -46,13 +64,15 @@ describe('RegistryConfigComponent (inline template)', () => {
|
||||
ReplicationConfigComponent,
|
||||
SystemSettingsComponent,
|
||||
VulnerabilityConfigComponent,
|
||||
RegistryConfigComponent
|
||||
RegistryConfigComponent,
|
||||
ConfirmationDialogComponent
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: ConfigurationService, useClass: ConfigurationDefaultService },
|
||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
|
||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
|
||||
{ provide: SystemInfoService, useClass: SystemInfoDefaultService }
|
||||
]
|
||||
});
|
||||
}));
|
||||
@ -62,30 +82,34 @@ describe('RegistryConfigComponent (inline template)', () => {
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
cfgService = fixture.debugElement.injector.get(ConfigurationService);
|
||||
systemInfoService = fixture.debugElement.injector.get(SystemInfoService);
|
||||
spy = spyOn(cfgService, 'getConfigurations').and.returnValue(Promise.resolve(mockConfig));
|
||||
saveSpy = spyOn(cfgService, 'saveConfigurations').and.returnValue(Promise.resolve(true));
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render configurations to the view', async(() => {
|
||||
expect(spy.calls.count()).toEqual(1);
|
||||
expect(spySystemInfo.calls.count()).toEqual(1);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLInputElement = fixture.nativeElement.querySelector('input[type="text"]');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el).not.toBeFalsy();
|
||||
expect(el.value).toEqual('30');
|
||||
|
||||
let el2: HTMLInputElement = fixture.nativeElement.querySelector('input[type="checkbox"]');
|
||||
expect(el2).toBeTruthy();
|
||||
expect(el2.value).toEqual('on');
|
||||
|
||||
fixture.detectChanges();
|
||||
let el3: HTMLInputElement = fixture.nativeElement.querySelector('input[type="time"]');
|
||||
expect(el3).toBeTruthy();
|
||||
expect(el3.value).toBeTruthy();
|
||||
expect(el3).not.toBeFalsy();
|
||||
});
|
||||
}));
|
||||
|
||||
|
@ -1,10 +1,22 @@
|
||||
import { Component, OnInit, EventEmitter, Output } from '@angular/core';
|
||||
import { Component, OnInit, EventEmitter, Output, ViewChild, Input } from '@angular/core';
|
||||
|
||||
import { Configuration, ComplexValueItem } from './config';
|
||||
import { REGISTRY_CONFIG_HTML } from './registry-config.component.html';
|
||||
import { ConfigurationService } from '../service/index';
|
||||
import { ConfigurationService, SystemInfoService, SystemInfo, ClairDBStatus } from '../service/index';
|
||||
import { toPromise } from '../utils';
|
||||
import { ErrorHandler } from '../error-handler';
|
||||
import {
|
||||
ReplicationConfigComponent,
|
||||
SystemSettingsComponent,
|
||||
VulnerabilityConfigComponent
|
||||
} from './index';
|
||||
|
||||
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
|
||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-registry-config',
|
||||
@ -13,27 +25,78 @@ import { ErrorHandler } from '../error-handler';
|
||||
export class RegistryConfigComponent implements OnInit {
|
||||
config: Configuration = new Configuration();
|
||||
configCopy: Configuration;
|
||||
onGoing: boolean = false;
|
||||
systemInfo: SystemInfo;
|
||||
|
||||
@Output() configChanged: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Input() hasAdminRole: boolean = false;
|
||||
|
||||
@ViewChild("replicationConfig") replicationCfg: ReplicationConfigComponent;
|
||||
@ViewChild("systemSettings") systemSettings: SystemSettingsComponent;
|
||||
@ViewChild("vulnerabilityConfig") vulnerabilityCfg: VulnerabilityConfigComponent;
|
||||
@ViewChild("cfgConfirmationDialog") confirmationDlg: ConfirmationDialogComponent;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigurationService,
|
||||
private errorHandler: ErrorHandler
|
||||
private errorHandler: ErrorHandler,
|
||||
private translate: TranslateService,
|
||||
private systemInfoService: SystemInfoService
|
||||
) { }
|
||||
|
||||
get shouldDisable(): boolean {
|
||||
return !this.isValid() || !this.hasChanges() || this.onGoing;
|
||||
}
|
||||
|
||||
get hasCAFile(): boolean {
|
||||
return this.systemInfo && this.systemInfo.has_ca_root;
|
||||
}
|
||||
|
||||
get withClair(): boolean {
|
||||
return this.systemInfo && this.systemInfo.with_clair;
|
||||
}
|
||||
|
||||
get clairDB(): ClairDBStatus {
|
||||
return this.systemInfo && this.systemInfo.clair_vulnerability_status ?
|
||||
this.systemInfo.clair_vulnerability_status : null;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
//Get system info
|
||||
toPromise<SystemInfo>(this.systemInfoService.getSystemInfo())
|
||||
.then((info: SystemInfo) => this.systemInfo = info)
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
|
||||
//Initialize
|
||||
this.load();
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
return this.replicationCfg &&
|
||||
this.replicationCfg.isValid &&
|
||||
this.systemSettings &&
|
||||
this.systemSettings.isValid &&
|
||||
this.vulnerabilityCfg &&
|
||||
this.vulnerabilityCfg.isValid;
|
||||
}
|
||||
|
||||
hasChanges(): boolean {
|
||||
return !this._isEmptyObject(this.getChanges());
|
||||
}
|
||||
|
||||
//Load configurations
|
||||
load(): void {
|
||||
this.onGoing = true;
|
||||
toPromise<Configuration>(this.configService.getConfigurations())
|
||||
.then((config: Configuration) => {
|
||||
this.configCopy = Object.assign({}, config);
|
||||
this.onGoing = false;
|
||||
|
||||
this.configCopy = this._clone(config);
|
||||
this.config = config;
|
||||
})
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
//Save configuration changes
|
||||
@ -45,26 +108,48 @@ export class RegistryConfigComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
//Fix policy parameters issue
|
||||
let scanningAllPolicy = changes["scan_all_policy"];
|
||||
if (scanningAllPolicy &&
|
||||
scanningAllPolicy.type !== "daily" &&
|
||||
scanningAllPolicy.parameters) {
|
||||
delete (scanningAllPolicy.parameters);
|
||||
}
|
||||
|
||||
this.onGoing = true;
|
||||
toPromise<any>(this.configService.saveConfigurations(changes))
|
||||
.then(() => {
|
||||
this.configChanged.emit(changes);
|
||||
this.onGoing = false;
|
||||
|
||||
this.translate.get("CONFIG.SAVE_SUCCESS").subscribe((res: string) => {
|
||||
this.errorHandler.info(res);
|
||||
});
|
||||
//Reload to fetch all the updates
|
||||
this.load();
|
||||
})
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
.catch(error => {
|
||||
this.onGoing = false;
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
//Cancel the changes if have
|
||||
cancel(): void {
|
||||
let msg = new ConfirmationMessage(
|
||||
"CONFIG.CONFIRM_TITLE",
|
||||
"CONFIG.CONFIRM_SUMMARY",
|
||||
"",
|
||||
{},
|
||||
ConfirmationTargets.CONFIG
|
||||
);
|
||||
this.confirmationDlg.open(msg);
|
||||
}
|
||||
|
||||
//Confirm cancel
|
||||
confirmCancel(ack: ConfirmationAcknowledgement): void {
|
||||
if (ack && ack.source === ConfirmationTargets.CONFIG &&
|
||||
ack.state === ConfirmationState.CONFIRMED) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
//Reset to the values of copy
|
||||
let changes: { [key: string]: any | any[] } = this.getChanges();
|
||||
for (let prop in changes) {
|
||||
this.config[prop] = Object.assign({}, this.configCopy[prop]);
|
||||
this.config[prop] = this._clone(this.configCopy[prop]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,4 +192,10 @@ export class RegistryConfigComponent implements OnInit {
|
||||
_isEmptyObject(obj: any): boolean {
|
||||
return !obj || JSON.stringify(obj) === "{}";
|
||||
}
|
||||
|
||||
//Deeper clone all
|
||||
_clone(srcObj: any): any {
|
||||
if (!srcObj) return null;
|
||||
return JSON.parse(JSON.stringify(srcObj));
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
export const REPLICATION_CONFIG_HTML: string = `
|
||||
<form #replicationConfigFrom="ngForm" class="compact">
|
||||
<section class="form-block" style="margin-top:0px;margin-bottom:0px;">
|
||||
<label style="font-size:14px;font-weight:600;">Image Replication</label>
|
||||
<label style="font-size:14px;font-weight:600;" *ngIf="showSubTitle">{{'CONFIG.REPLICATION' | translate}}</label>
|
||||
<div class="form-group">
|
||||
<label for="verifyRemoteCert">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
|
||||
<clr-checkbox name="verifyRemoteCert" id="verifyRemoteCert" [(ngModel)]="replicationConfig.verify_remote_cert.value" [disabled]="!editable">
|
||||
|
@ -3,10 +3,12 @@ import { NgForm } from '@angular/forms';
|
||||
|
||||
import { REPLICATION_CONFIG_HTML } from './replication-config.component.html';
|
||||
import { Configuration } from '../config';
|
||||
import { REGISTRY_CONFIG_STYLES } from '../registry-config.component.css';
|
||||
|
||||
@Component({
|
||||
selector: 'replication-config',
|
||||
template: REPLICATION_CONFIG_HTML
|
||||
template: REPLICATION_CONFIG_HTML,
|
||||
styles: [REGISTRY_CONFIG_STYLES]
|
||||
})
|
||||
export class ReplicationConfigComponent {
|
||||
config: Configuration;
|
||||
@ -21,6 +23,8 @@ export class ReplicationConfigComponent {
|
||||
this.configChange.emit(this.config);
|
||||
}
|
||||
|
||||
@Input() showSubTitle: boolean = false
|
||||
|
||||
@ViewChild("replicationConfigFrom") replicationConfigForm: NgForm;
|
||||
|
||||
get editable(): boolean {
|
||||
|
@ -1,7 +1,7 @@
|
||||
export const SYSTEM_SETTINGS_HTML: string = `
|
||||
<form #systemConfigFrom="ngForm" class="compact">
|
||||
<section class="form-block" style="margin-top:0px;margin-bottom:0px;">
|
||||
<label style="font-size:14px;font-weight:600;">System Settings</label>
|
||||
<label style="font-size:14px;font-weight:600;" *ngIf="showSubTitle">{{'CONFIG.SYSTEM' | translate}}</label>
|
||||
<div class="form-group">
|
||||
<label for="tokenExpiration" class="required">{{'CONFIG.TOKEN_EXPIRATION' | translate}}</label>
|
||||
<label for="tokenExpiration" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right" [class.invalid]="tokenExpirationInput.invalid && (tokenExpirationInput.dirty || tokenExpirationInput.touched)">
|
||||
@ -19,6 +19,14 @@ export const SYSTEM_SETTINGS_HTML: string = `
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.TOKEN_EXPIRATION' | translate}}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="canDownloadCert">
|
||||
<label for="certDownloadLink" class="required">{{'CONFIG.ROOT_CERT' | translate}}</label>
|
||||
<a #certDownloadLink href="/api/systeminfo/getcert" target="_blank">{{'CONFIG.ROOT_CERT_LINK' | translate}}</a>
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.ROOT_CERT_DOWNLOAD' | translate}}</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
`;
|
@ -3,10 +3,12 @@ import { NgForm } from '@angular/forms';
|
||||
|
||||
import { SYSTEM_SETTINGS_HTML } from './system-settings.component.html';
|
||||
import { Configuration } from '../config';
|
||||
import { REGISTRY_CONFIG_STYLES } from '../registry-config.component.css';
|
||||
|
||||
@Component({
|
||||
selector: 'system-settings',
|
||||
template: SYSTEM_SETTINGS_HTML
|
||||
template: SYSTEM_SETTINGS_HTML,
|
||||
styles: [REGISTRY_CONFIG_STYLES]
|
||||
})
|
||||
export class SystemSettingsComponent {
|
||||
config: Configuration;
|
||||
@ -21,6 +23,10 @@ export class SystemSettingsComponent {
|
||||
this.configChange.emit(this.config);
|
||||
}
|
||||
|
||||
@Input() showSubTitle: boolean = false;
|
||||
@Input() hasAdminRole: boolean = false;
|
||||
@Input() hasCAFile: boolean = false;
|
||||
|
||||
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
|
||||
|
||||
get editable(): boolean {
|
||||
@ -32,4 +38,8 @@ export class SystemSettingsComponent {
|
||||
get isValid(): boolean {
|
||||
return this.systemSettingsForm && this.systemSettingsForm.valid;
|
||||
}
|
||||
|
||||
get canDownloadCert(): boolean {
|
||||
return this.hasAdminRole && this.hasCAFile;
|
||||
}
|
||||
}
|
@ -1,17 +1,36 @@
|
||||
export const VULNERABILITY_CONFIG_HTML: string = `
|
||||
<form #systemConfigFrom="ngForm" class="compact">
|
||||
<section class="form-block" style="margin-top:0px;margin-bottom:0px;">
|
||||
<label class="section-title">{{ 'CONFIG.SCANNING.TITLE' | translate }}</label>
|
||||
<label class="section-title" *ngIf="showSubTitle">{{ 'CONFIG.SCANNING.TITLE' | translate }}</label>
|
||||
<div class="form-group">
|
||||
<label>{{ 'CONFIG.SCANNING.DB_REFRESH_TIME' | translate }}</label>
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-right'" style="margin-top:-8px;" class="clr-dropdown-override">
|
||||
<button class="btn btn-link btn-font" clrDropdownToggle>
|
||||
{{ updatedTimestamp }}
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu" style="min-width:300px;">
|
||||
<div *ngFor="let nt of namespaceTimestamps" class="namespace">
|
||||
<span class="label label-info">{{nt.namespace}}</span>
|
||||
<span>{{convertToLocalTime(nt.last_update*1000)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="scanAllPolicy">{{ 'CONFIG.SCANNING.SCAN_ALL' | translate }}</label>
|
||||
<div class="select">
|
||||
<select id="scanAllPolicy" name="scanAllPolicy" [disabled]="!editable" [(ngModel)]="vulnerabilityConfig.scan_all_policy.value.type">
|
||||
<select id="scanAllPolicy" name="scanAllPolicy" [disabled]="!editable" [(ngModel)]="scanningType">
|
||||
<option value="none">{{ 'CONFIG.SCANNING.NONE_POLICY' | translate }}</option>
|
||||
<option value="daily">{{ 'CONFIG.SCANNING.DAILY_POLICY' | translate }}</option>
|
||||
<option value="on_refresh">{{ 'CONFIG.SCANNING.REFRESH_POLICY' | translate }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="time" name="dailyTimePicker" [disabled]="!editable" [hidden]="!showTimePicker" [(ngModel)]="dailyTime" />
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.SCANNING_POLICY' | translate}}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="form-group form-group-override">
|
||||
<button class="btn btn-primary btn-sm" style="width:160px;" (click)="scanNow()">{{ 'CONFIG.SCANNING.SCAN_NOW' | translate }}</button>
|
||||
@ -29,4 +48,16 @@ export const VULNERABILITY_CONFIG_STYLES: string = `
|
||||
font-size: 14px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.btn-font {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.clr-dropdown-override {
|
||||
margin-top: -8px;
|
||||
}
|
||||
`;
|
@ -7,6 +7,9 @@ import { ScanningResultService } from '../../service/scanning.service';
|
||||
import { ErrorHandler } from '../../error-handler';
|
||||
import { toPromise } from '../../utils';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ClairDBStatus, ClairDetail } from '../../service/interface';
|
||||
|
||||
import { REGISTRY_CONFIG_STYLES } from '../registry-config.component.css';
|
||||
|
||||
const ONE_HOUR_SECONDS: number = 3600;
|
||||
const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
|
||||
@ -14,12 +17,13 @@ const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
|
||||
@Component({
|
||||
selector: 'vulnerability-config',
|
||||
template: VULNERABILITY_CONFIG_HTML,
|
||||
styles: [VULNERABILITY_CONFIG_STYLES]
|
||||
styles: [VULNERABILITY_CONFIG_STYLES, REGISTRY_CONFIG_STYLES]
|
||||
})
|
||||
export class VulnerabilityConfigComponent {
|
||||
_localTime: Date = new Date();
|
||||
|
||||
config: Configuration;
|
||||
openState: boolean = false;
|
||||
@Output() configChange: EventEmitter<Configuration> = new EventEmitter<Configuration>();
|
||||
|
||||
@Input()
|
||||
@ -30,17 +34,38 @@ export class VulnerabilityConfigComponent {
|
||||
this.config = cfg;
|
||||
if (this.config.scan_all_policy &&
|
||||
this.config.scan_all_policy.value) {
|
||||
if (this.config.scan_all_policy.value.type === "daily"){
|
||||
if(!this.config.scan_all_policy.value.parameters){
|
||||
this.config.scan_all_policy.value.parameters = {
|
||||
daily_time: 0
|
||||
};
|
||||
}
|
||||
if (this.config.scan_all_policy.value.type === "daily") {
|
||||
if (!this.config.scan_all_policy.value.parameter) {
|
||||
this.config.scan_all_policy.value.parameter = {
|
||||
daily_time: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
this.configChange.emit(this.config);
|
||||
}
|
||||
|
||||
@Input() showSubTitle: boolean = false;
|
||||
@Input() clairDBStatus: ClairDBStatus;
|
||||
|
||||
get updatedTimestamp(): string {
|
||||
if (this.clairDBStatus && this.clairDBStatus.overall_last_update > 0) {
|
||||
return this.convertToLocalTime(this.clairDBStatus.overall_last_update*1000);
|
||||
}
|
||||
|
||||
return "--";
|
||||
}
|
||||
|
||||
get namespaceTimestamps(): ClairDetail[] {
|
||||
if (this.clairDBStatus &&
|
||||
this.clairDBStatus.details &&
|
||||
this.clairDBStatus.details.length > 0) {
|
||||
return this.clairDBStatus.details;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
//UTC time
|
||||
get dailyTime(): string {
|
||||
if (!(this.config &&
|
||||
@ -51,8 +76,8 @@ export class VulnerabilityConfigComponent {
|
||||
}
|
||||
|
||||
let timeOffset: number = 0;//seconds
|
||||
if (this.config.scan_all_policy.value.parameters) {
|
||||
let daily_time = this.config.scan_all_policy.value.parameters.daily_time;
|
||||
if (this.config.scan_all_policy.value.parameter) {
|
||||
let daily_time = this.config.scan_all_policy.value.parameter.daily_time;
|
||||
if (daily_time && typeof daily_time === "number") {
|
||||
timeOffset = +daily_time;
|
||||
}
|
||||
@ -99,8 +124,9 @@ export class VulnerabilityConfigComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.config.scan_all_policy.value.parameters) {
|
||||
this.config.scan_all_policy.value.parameters = {
|
||||
//Double confirm inner parameter existing.
|
||||
if (!this.config.scan_all_policy.value.parameter) {
|
||||
this.config.scan_all_policy.value.parameter = {
|
||||
daily_time: 0
|
||||
};
|
||||
}
|
||||
@ -124,7 +150,41 @@ export class VulnerabilityConfigComponent {
|
||||
utcTimes -= ONE_DAY_SECONDS;
|
||||
}
|
||||
|
||||
this.config.scan_all_policy.value.parameters.daily_time = utcTimes;
|
||||
this.config.scan_all_policy.value.parameter.daily_time = utcTimes;
|
||||
}
|
||||
|
||||
//Scanning type
|
||||
get scanningType(): string {
|
||||
if (this.config &&
|
||||
this.config.scan_all_policy &&
|
||||
this.config.scan_all_policy.value) {
|
||||
return this.config.scan_all_policy.value.type;
|
||||
} else {
|
||||
//default
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
set scanningType(v: string) {
|
||||
if (this.config &&
|
||||
this.config.scan_all_policy &&
|
||||
this.config.scan_all_policy.value) {
|
||||
let type: string = (v && v.trim() !== "") ? v : "none";
|
||||
this.config.scan_all_policy.value.type = type;
|
||||
if (type !== "daily") {
|
||||
//No parameter
|
||||
if (this.config.scan_all_policy.value.parameter) {
|
||||
delete (this.config.scan_all_policy.value.parameter);
|
||||
}
|
||||
} else {
|
||||
//Has parameter
|
||||
if (!this.config.scan_all_policy.value.parameter) {
|
||||
this.config.scan_all_policy.value.parameter = {
|
||||
daily_time: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewChild("systemConfigFrom") systemSettingsForm: NgForm;
|
||||
@ -151,6 +211,14 @@ export class VulnerabilityConfigComponent {
|
||||
private errorHandler: ErrorHandler,
|
||||
private translate: TranslateService) { }
|
||||
|
||||
convertToLocalTime(utcTime: number): string {
|
||||
let offset: number = this._localTime.getTimezoneOffset() * 60;
|
||||
let timeWithLocal: number = utcTime - offset;
|
||||
let dt = new Date();
|
||||
dt.setTime(timeWithLocal);
|
||||
return dt.toLocaleString();
|
||||
}
|
||||
|
||||
scanNow(): void {
|
||||
toPromise<any>(this.scanningService.startScanningAll())
|
||||
.then(() => {
|
||||
|
@ -3,7 +3,6 @@ export const REPOSITORY_STACKVIEW_STYLES: string = `
|
||||
padding-right: 16px;
|
||||
}
|
||||
.sub-grid-custom {
|
||||
position: relative;
|
||||
left: 40px;
|
||||
}
|
||||
.refresh-btn {
|
||||
|
@ -155,6 +155,23 @@ export interface SystemInfo {
|
||||
self_registration?: boolean;
|
||||
has_ca_root?: boolean;
|
||||
harbor_version?: string;
|
||||
clair_vulnerability_status?: ClairDBStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clair database status info.
|
||||
*
|
||||
* @export
|
||||
* @interface ClairDetail
|
||||
*/
|
||||
export interface ClairDetail {
|
||||
namespace: string;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface ClairDBStatus {
|
||||
overall_last_update: number;
|
||||
details: ClairDetail[];
|
||||
}
|
||||
|
||||
export enum VulnerabilitySeverity {
|
||||
|
@ -5,6 +5,7 @@ export const TAG_STYLE = `
|
||||
|
||||
.embeded-datagrid {
|
||||
width: 98%;
|
||||
float:right; /*add for issue #2688*/
|
||||
}
|
||||
|
||||
.hidden-tag {
|
||||
|
@ -167,7 +167,7 @@ export class TagComponent implements OnInit, OnDestroy {
|
||||
this.loading = false;
|
||||
});
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
setTimeout(() => clearInterval(hnd), 5000);
|
||||
}
|
||||
|
||||
deleteTag(tag: Tag) {
|
||||
@ -272,4 +272,4 @@ export class TagComponent implements OnInit, OnDestroy {
|
||||
this.textInput.nativeElement.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
||||
"clarity-icons": "^0.9.8",
|
||||
"clarity-ui": "^0.9.8",
|
||||
"core-js": "^2.4.1",
|
||||
"harbor-ui": "^0.2.40",
|
||||
"harbor-ui": "~0.2.63",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
@ -68,4 +68,4 @@
|
||||
"typings": "^1.4.0",
|
||||
"webdriver-manager": "10.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,22 +11,30 @@
|
||||
// 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 { ClairDBStatus } from 'harbor-ui';
|
||||
|
||||
export class AppConfig {
|
||||
constructor(){
|
||||
constructor() {
|
||||
//Set default value
|
||||
this.with_notary = false;
|
||||
this.with_admiral = false;
|
||||
this.with_clair = false;
|
||||
this.admiral_endpoint = "";
|
||||
this.auth_mode = "db_auth";
|
||||
this.registry_url = "";
|
||||
this.project_creation_restriction = "everyone";
|
||||
this.self_registration = true;
|
||||
this.has_ca_root = false;
|
||||
this.harbor_version = "0.5.0";//default
|
||||
this.harbor_version = "1.2.0";//default
|
||||
this.clair_vulnerability_status = {
|
||||
overall_last_update: 0,
|
||||
details: []
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
with_notary: boolean;
|
||||
with_admiral: boolean;
|
||||
with_clair: boolean;
|
||||
admiral_endpoint: string;
|
||||
auth_mode: string;
|
||||
registry_url: string;
|
||||
@ -34,4 +42,5 @@ export class AppConfig {
|
||||
self_registration: boolean;
|
||||
has_ca_root: boolean;
|
||||
harbor_version: string;
|
||||
clair_vulnerability_status?: ClairDBStatus;
|
||||
}
|
@ -15,8 +15,8 @@
|
||||
<li role="presentation" class="nav-item">
|
||||
<button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")' type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button>
|
||||
</li>
|
||||
<li role="presentation" class="nav-item">
|
||||
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>Vulnerability</button>
|
||||
<li role="presentation" class="nav-item" *ngIf="withClair">
|
||||
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>{{'VULNERABILITY.SINGULAR' | translate}}</button>
|
||||
</li>
|
||||
</ul>
|
||||
<section id="authentication" role="tabpanel" aria-labelledby="config-auth" [hidden]='!isCurrentTabContent("authentication")'>
|
||||
@ -29,10 +29,10 @@
|
||||
<config-email [mailConfig]="allConfig"></config-email>
|
||||
</section>
|
||||
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
|
||||
<system-settings [(systemSettings)]="allConfig"></system-settings>
|
||||
<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>
|
||||
</section>
|
||||
<section id="vulnerability" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
|
||||
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
|
||||
<section id="vulnerability" *ngIf="withClair" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
|
||||
<vulnerability-config [(vulnerabilityConfig)]="allConfig" [clairDBStatus]="clairDB"></vulnerability-config>
|
||||
</section>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
|
||||
@ -41,7 +41,6 @@
|
||||
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
|
||||
<span id="forTestingMail" class="spinner spinner-inline" [hidden]="hideMailTestingSpinner"></span>
|
||||
<span id="forTestingLDAP" class="spinner spinner-inline" [hidden]="hideLDAPTestingSpinner"></span>
|
||||
<button type="button" class="btn btn-primary" (click)="consoleTest()">CONSOLE</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -32,7 +32,8 @@ import {
|
||||
ComplexValueItem,
|
||||
ReplicationConfigComponent,
|
||||
SystemSettingsComponent,
|
||||
VulnerabilityConfigComponent
|
||||
VulnerabilityConfigComponent,
|
||||
ClairDBStatus
|
||||
} from 'harbor-ui';
|
||||
|
||||
const fakePass = "aWpLOSYkIzJTTU4wMDkx";
|
||||
@ -71,11 +72,23 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
private appConfigService: AppConfigService,
|
||||
private session: SessionService) { }
|
||||
|
||||
consoleTest(): void {
|
||||
console.log(this.allConfig, this.originalCopy);
|
||||
console.log("-------------");
|
||||
console.log(this.getChanges());
|
||||
public get hasAdminRole(): boolean {
|
||||
return this.session.getCurrentUser() &&
|
||||
this.session.getCurrentUser().has_admin_role > 0;
|
||||
}
|
||||
|
||||
public get hasCAFile(): boolean {
|
||||
return this.appConfigService.getConfig().has_ca_root;
|
||||
}
|
||||
|
||||
public get withClair(): boolean {
|
||||
return this.appConfigService.getConfig().with_clair;
|
||||
}
|
||||
|
||||
public get clairDB(): ClairDBStatus {
|
||||
return this.appConfigService.getConfig().clair_vulnerability_status;
|
||||
}
|
||||
|
||||
isCurrentTabLink(tabId: string): boolean {
|
||||
return this.currentTabId === tabId;
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export class ListProjectROComponent {
|
||||
goToLink(proId: number): void {
|
||||
this.searchTrigger.closeSearch(true);
|
||||
|
||||
let linkUrl = ['harbor', 'projects', proId, 'repository'];
|
||||
let linkUrl = ['harbor', 'projects', proId, 'repositories'];
|
||||
this.router.navigate(linkUrl);
|
||||
}
|
||||
|
||||
|
@ -83,8 +83,7 @@
|
||||
"PROFILE": "User Profile",
|
||||
"CHANGE_PWD": "Change Password",
|
||||
"ABOUT": "About",
|
||||
"LOGOUT": "Log Out",
|
||||
"ROOT_CERT": "Download Root Cert"
|
||||
"LOGOUT": "Log Out"
|
||||
},
|
||||
"GLOBAL_SEARCH": {
|
||||
"PLACEHOLDER": "Search Harbor...",
|
||||
@ -383,6 +382,8 @@
|
||||
"SCOPE_SUBTREE": "Subtree",
|
||||
"PRO_CREATION_EVERYONE": "Everyone",
|
||||
"PRO_CREATION_ADMIN": "Admin Only",
|
||||
"ROOT_CERT": "Registry Root Certificate",
|
||||
"ROOT_CERT_LINK": "Download",
|
||||
"TOOLTIP": {
|
||||
"SELF_REGISTRATION": "Enable sign up.",
|
||||
"VERIFY_REMOTE_CERT": "Determine whether the image replication should verify the certificate of a remote Harbor registry. Uncheck this box when the remote registry uses a self-signed or untrusted certificate.",
|
||||
@ -390,9 +391,11 @@
|
||||
"LDAP_SEARCH_DN": "A user's DN who has the permission to search the LDAP/AD server. If your LDAP/AD server does not support anonymous search, you should configure this DN and ldap_search_pwd.",
|
||||
"LDAP_BASE_DN": "The base DN from which to look up a user in LDAP/AD.",
|
||||
"LDAP_UID": "The attribute used in a search to match a user. It could be uid, cn, email, sAMAccountName or other attributes depending on your LDAP/AD.",
|
||||
"LDAP_SCOPE": "The scope to search for users",
|
||||
"LDAP_SCOPE": "The scope to search for users.",
|
||||
"TOKEN_EXPIRATION": "The expiration time (in minutes) of a token created by the token service. Default is 30 minutes.",
|
||||
"PRO_CREATION_RESTRICTION": "The flag to define what users have permission to create projects. By default, everyone can create a project. Set to 'Admin Only' so that only an administrator can create a project."
|
||||
"PRO_CREATION_RESTRICTION": "The flag to define what users have permission to create projects. By default, everyone can create a project. Set to 'Admin Only' so that only an administrator can create a project.",
|
||||
"ROOT_CERT_DOWNLOAD": "Download the root certificate of registry.",
|
||||
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday; 'Upon Refresh': Triggering scanning when database refreshed."
|
||||
},
|
||||
"LDAP": {
|
||||
"URL": "LDAP URL",
|
||||
@ -410,7 +413,9 @@
|
||||
"SCAN_NOW": "SCAN NOW",
|
||||
"NONE_POLICY": "None",
|
||||
"DAILY_POLICY": "Daily At",
|
||||
"REFRESH_POLICY": "Upon Refresh"
|
||||
"REFRESH_POLICY": "Upon Refresh",
|
||||
"DB_REFRESH_TIME": "Database updated on",
|
||||
"DB_NOT_READY": "Vulnerability database might not be fully ready!"
|
||||
},
|
||||
"TEST_MAIL_SUCCESS": "Connection to mail server is verified.",
|
||||
"TEST_LDAP_SUCCESS": "Connection to LDAP server is verified.",
|
||||
|
@ -83,8 +83,7 @@
|
||||
"PROFILE": "Perfil de usuario",
|
||||
"CHANGE_PWD": "Cambiar contraseña",
|
||||
"ABOUT": "Acerca de",
|
||||
"LOGOUT": "Cerrar sesión",
|
||||
"ROOT_CERT": "Descargar Certificado Raíz"
|
||||
"LOGOUT": "Cerrar sesión"
|
||||
},
|
||||
"GLOBAL_SEARCH": {
|
||||
"PLACEHOLDER": "Buscar en Harbor...",
|
||||
@ -384,6 +383,8 @@
|
||||
"SCOPE_SUBTREE": "Subárbol",
|
||||
"PRO_CREATION_EVERYONE": "Todos",
|
||||
"PRO_CREATION_ADMIN": "Solo Administradores",
|
||||
"ROOT_CERT": "Registro Certificado Raíz",
|
||||
"ROOT_CERT_LINK": "Descargar",
|
||||
"TOOLTIP": {
|
||||
"SELF_REGISTRATION": "Activar registro.",
|
||||
"VERIFY_REMOTE_CERT": "Determina si la replicación de la imagen debería verificar el certificado de un registro Harbor remoto. Desmarque esta opción cuando el registro remoto use un certificado de confianza o autofirmado.",
|
||||
@ -393,7 +394,9 @@
|
||||
"LDAP_UID": "El atributo usado en una búsqueda para encontrar un usuario. Debe ser el uid, cn, email, sAMAccountName u otro atributo dependiendo del LDAP/AD.",
|
||||
"LDAP_SCOPE": "El ámbito de búsqueda para usuarios",
|
||||
"TOKEN_EXPIRATION": "El tiempo de expiración (en minutos) del token creado por el servicio de tokens. Por defecto son 30 minutos.",
|
||||
"PRO_CREATION_RESTRICTION": "Marca para definir qué usuarios tienen permisos para crear proyectos. Por defecto, todos pueden crear proyectos. Seleccione 'Solo Administradores' para que solamente los administradores puedan crear proyectos."
|
||||
"PRO_CREATION_RESTRICTION": "Marca para definir qué usuarios tienen permisos para crear proyectos. Por defecto, todos pueden crear proyectos. Seleccione 'Solo Administradores' para que solamente los administradores puedan crear proyectos.",
|
||||
"ROOT_CERT_DOWNLOAD": "Download the root certificate of registry.",
|
||||
"SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday; 'Upon Refresh': Triggering scanning when database refreshed."
|
||||
},
|
||||
"LDAP": {
|
||||
"URL": "LDAP URL",
|
||||
@ -411,7 +414,9 @@
|
||||
"SCAN_NOW": "SCAN NOW",
|
||||
"NONE_POLICY": "None",
|
||||
"DAILY_POLICY": "Daily At",
|
||||
"REFRESH_POLICY": "Upon Refresh"
|
||||
"REFRESH_POLICY": "Upon Refresh",
|
||||
"DB_REFRESH_TIME": "Database updated on",
|
||||
"DB_NOT_READY": "Vulnerability database might not be fully ready!"
|
||||
},
|
||||
"TEST_MAIL_SUCCESS": "La conexión al servidor de correo ha sido verificada.",
|
||||
"TEST_LDAP_SUCCESS": "La conexión al servidor LDAP ha sido verificada.",
|
||||
|
@ -83,8 +83,7 @@
|
||||
"PROFILE": "用户设置",
|
||||
"CHANGE_PWD": "修改密码",
|
||||
"ABOUT": "关于",
|
||||
"LOGOUT": "退出",
|
||||
"ROOT_CERT": "下载根证书"
|
||||
"LOGOUT": "退出"
|
||||
},
|
||||
"GLOBAL_SEARCH": {
|
||||
"PLACEHOLDER": "搜索 Harbor...",
|
||||
@ -383,6 +382,8 @@
|
||||
"SCOPE_SUBTREE": "子树",
|
||||
"PRO_CREATION_EVERYONE": "所有人",
|
||||
"PRO_CREATION_ADMIN": "仅管理员",
|
||||
"ROOT_CERT": "镜像库根证书",
|
||||
"ROOT_CERT_LINK": "下载",
|
||||
"TOOLTIP": {
|
||||
"SELF_REGISTRATION": "激活注册功能。",
|
||||
"VERIFY_REMOTE_CERT": "确定镜像复制是否要验证远程Harbor实例的证书。如果远程实例使用的是自签或者非信任证书,不要勾选此项。",
|
||||
@ -392,7 +393,9 @@
|
||||
"LDAP_UID": "在搜索中用来匹配用户的属性,可以是uid,cn,email,sAMAccountName或者其它LDAP/AD服务器支持的属性。",
|
||||
"LDAP_SCOPE": "搜索用户的范围。",
|
||||
"TOKEN_EXPIRATION": "由令牌服务创建的令牌的过期时间(分钟),默认为30分钟。",
|
||||
"PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。"
|
||||
"PRO_CREATION_RESTRICTION": "用来确定哪些用户有权限创建项目,默认为’所有人‘,设置为’仅管理员‘则只有管理员可以创建项目。",
|
||||
"ROOT_CERT_DOWNLOAD": "下载镜像库根证书.",
|
||||
"SCANNING_POLICY": "基于不同需求设置镜像扫描策略。‘无’:不设置任何策略;‘每日定时’:每天在设置的时间定时执行扫描;‘缺陷库刷新后’:当缺陷数据库刷新后。"
|
||||
},
|
||||
"LDAP": {
|
||||
"URL": "LDAP URL",
|
||||
@ -409,8 +412,10 @@
|
||||
"SCAN_ALL": "扫描所有",
|
||||
"SCAN_NOW": "开始扫描",
|
||||
"NONE_POLICY": "无",
|
||||
"DAILY_POLICY": "每日",
|
||||
"REFRESH_POLICY": "缺陷库刷新后"
|
||||
"DAILY_POLICY": "每日定时",
|
||||
"REFRESH_POLICY": "缺陷库刷新后",
|
||||
"DB_REFRESH_TIME": "数据库更新于",
|
||||
"DB_NOT_READY": "缺陷数据库可能没有完全准备好!"
|
||||
},
|
||||
"TEST_MAIL_SUCCESS": "邮件服务器的连通正常。",
|
||||
"TEST_LDAP_SUCCESS": "LDAP服务器的连通正常。",
|
||||
|
3
src/vendor/github.com/astaxie/beego/orm/orm.go
generated
vendored
3
src/vendor/github.com/astaxie/beego/orm/orm.go
generated
vendored
@ -137,11 +137,10 @@ func (o *orm) ReadOrCreate(md interface{}, col1 string, cols ...string) (bool, i
|
||||
if err == ErrNoRows {
|
||||
// Create
|
||||
id, err := o.Insert(md)
|
||||
fmt.Printf("id when create: %d", id)
|
||||
return (err == nil), id, err
|
||||
}
|
||||
|
||||
return false, 0, err
|
||||
return false, ind.FieldByIndex(mi.fields.pk.fieldIndex).Int(), err
|
||||
}
|
||||
|
||||
// insert model data to database
|
||||
|
29
src/vendor/github.com/lib/pq/CONTRIBUTING.md
generated
vendored
Normal file
29
src/vendor/github.com/lib/pq/CONTRIBUTING.md
generated
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
## Contributing to pq
|
||||
|
||||
`pq` has a backlog of pull requests, but contributions are still very
|
||||
much welcome. You can help with patch review, submitting bug reports,
|
||||
or adding new functionality. There is no formal style guide, but
|
||||
please conform to the style of existing code and general Go formatting
|
||||
conventions when submitting patches.
|
||||
|
||||
### Patch review
|
||||
|
||||
Help review existing open pull requests by commenting on the code or
|
||||
proposed functionality.
|
||||
|
||||
### Bug reports
|
||||
|
||||
We appreciate any bug reports, but especially ones with self-contained
|
||||
(doesn't depend on code outside of pq), minimal (can't be simplified
|
||||
further) test cases. It's especially helpful if you can submit a pull
|
||||
request with just the failing test case (you'll probably want to
|
||||
pattern it after the tests in
|
||||
[conn_test.go](https://github.com/lib/pq/blob/master/conn_test.go).
|
||||
|
||||
### New functionality
|
||||
|
||||
There are a number of pending patches for new functionality, so
|
||||
additional feature patches will take a while to merge. Still, patches
|
||||
are generally reviewed based on usefulness and complexity in addition
|
||||
to time-in-queue, so if you have a knockout idea, take a shot. Feel
|
||||
free to open an issue discussion your proposed patch beforehand.
|
8
src/vendor/github.com/lib/pq/LICENSE.md
generated
vendored
Normal file
8
src/vendor/github.com/lib/pq/LICENSE.md
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
Copyright (c) 2011-2013, 'pq' Contributors
|
||||
Portions Copyright (C) 2011 Blake Mizerany
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
105
src/vendor/github.com/lib/pq/README.md
generated
vendored
Normal file
105
src/vendor/github.com/lib/pq/README.md
generated
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
# pq - A pure Go postgres driver for Go's database/sql package
|
||||
|
||||
[![Build Status](https://travis-ci.org/lib/pq.svg?branch=master)](https://travis-ci.org/lib/pq)
|
||||
|
||||
## Install
|
||||
|
||||
go get github.com/lib/pq
|
||||
|
||||
## Docs
|
||||
|
||||
For detailed documentation and basic usage examples, please see the package
|
||||
documentation at <http://godoc.org/github.com/lib/pq>.
|
||||
|
||||
## Tests
|
||||
|
||||
`go test` is used for testing. A running PostgreSQL server is
|
||||
required, with the ability to log in. The default database to connect
|
||||
to test with is "pqgotest," but it can be overridden using environment
|
||||
variables.
|
||||
|
||||
Example:
|
||||
|
||||
PGHOST=/run/postgresql go test github.com/lib/pq
|
||||
|
||||
Optionally, a benchmark suite can be run as part of the tests:
|
||||
|
||||
PGHOST=/run/postgresql go test -bench .
|
||||
|
||||
## Features
|
||||
|
||||
* SSL
|
||||
* Handles bad connections for `database/sql`
|
||||
* Scan `time.Time` correctly (i.e. `timestamp[tz]`, `time[tz]`, `date`)
|
||||
* Scan binary blobs correctly (i.e. `bytea`)
|
||||
* Package for `hstore` support
|
||||
* COPY FROM support
|
||||
* pq.ParseURL for converting urls to connection strings for sql.Open.
|
||||
* Many libpq compatible environment variables
|
||||
* Unix socket support
|
||||
* Notifications: `LISTEN`/`NOTIFY`
|
||||
* pgpass support
|
||||
|
||||
## Future / Things you can help with
|
||||
|
||||
* Better COPY FROM / COPY TO (see discussion in #181)
|
||||
|
||||
## Thank you (alphabetical)
|
||||
|
||||
Some of these contributors are from the original library `bmizerany/pq.go` whose
|
||||
code still exists in here.
|
||||
|
||||
* Andy Balholm (andybalholm)
|
||||
* Ben Berkert (benburkert)
|
||||
* Benjamin Heatwole (bheatwole)
|
||||
* Bill Mill (llimllib)
|
||||
* Bjørn Madsen (aeons)
|
||||
* Blake Gentry (bgentry)
|
||||
* Brad Fitzpatrick (bradfitz)
|
||||
* Charlie Melbye (cmelbye)
|
||||
* Chris Bandy (cbandy)
|
||||
* Chris Gilling (cgilling)
|
||||
* Chris Walsh (cwds)
|
||||
* Dan Sosedoff (sosedoff)
|
||||
* Daniel Farina (fdr)
|
||||
* Eric Chlebek (echlebek)
|
||||
* Eric Garrido (minusnine)
|
||||
* Eric Urban (hydrogen18)
|
||||
* Everyone at The Go Team
|
||||
* Evan Shaw (edsrzf)
|
||||
* Ewan Chou (coocood)
|
||||
* Fazal Majid (fazalmajid)
|
||||
* Federico Romero (federomero)
|
||||
* Fumin (fumin)
|
||||
* Gary Burd (garyburd)
|
||||
* Heroku (heroku)
|
||||
* James Pozdena (jpoz)
|
||||
* Jason McVetta (jmcvetta)
|
||||
* Jeremy Jay (pbnjay)
|
||||
* Joakim Sernbrant (serbaut)
|
||||
* John Gallagher (jgallagher)
|
||||
* Jonathan Rudenberg (titanous)
|
||||
* Joël Stemmer (jstemmer)
|
||||
* Kamil Kisiel (kisielk)
|
||||
* Kelly Dunn (kellydunn)
|
||||
* Keith Rarick (kr)
|
||||
* Kir Shatrov (kirs)
|
||||
* Lann Martin (lann)
|
||||
* Maciek Sakrejda (uhoh-itsmaciek)
|
||||
* Marc Brinkmann (mbr)
|
||||
* Marko Tiikkaja (johto)
|
||||
* Matt Newberry (MattNewberry)
|
||||
* Matt Robenolt (mattrobenolt)
|
||||
* Martin Olsen (martinolsen)
|
||||
* Mike Lewis (mikelikespie)
|
||||
* Nicolas Patry (Narsil)
|
||||
* Oliver Tonnhofer (olt)
|
||||
* Patrick Hayes (phayes)
|
||||
* Paul Hammond (paulhammond)
|
||||
* Ryan Smith (ryandotsmith)
|
||||
* Samuel Stauffer (samuel)
|
||||
* Timothée Peignier (cyberdelia)
|
||||
* Travis Cline (tmc)
|
||||
* TruongSinh Tran-Nguyen (truongsinh)
|
||||
* Yaismel Miranda (ympons)
|
||||
* notedit (notedit)
|
756
src/vendor/github.com/lib/pq/array.go
generated
vendored
Normal file
756
src/vendor/github.com/lib/pq/array.go
generated
vendored
Normal file
@ -0,0 +1,756 @@
|
||||
package pq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var typeByteSlice = reflect.TypeOf([]byte{})
|
||||
var typeDriverValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
|
||||
var typeSqlScanner = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
|
||||
|
||||
// Array returns the optimal driver.Valuer and sql.Scanner for an array or
|
||||
// slice of any dimension.
|
||||
//
|
||||
// For example:
|
||||
// db.Query(`SELECT * FROM t WHERE id = ANY($1)`, pq.Array([]int{235, 401}))
|
||||
//
|
||||
// var x []sql.NullInt64
|
||||
// db.QueryRow('SELECT ARRAY[235, 401]').Scan(pq.Array(&x))
|
||||
//
|
||||
// Scanning multi-dimensional arrays is not supported. Arrays where the lower
|
||||
// bound is not one (such as `[0:0]={1}') are not supported.
|
||||
func Array(a interface{}) interface {
|
||||
driver.Valuer
|
||||
sql.Scanner
|
||||
} {
|
||||
switch a := a.(type) {
|
||||
case []bool:
|
||||
return (*BoolArray)(&a)
|
||||
case []float64:
|
||||
return (*Float64Array)(&a)
|
||||
case []int64:
|
||||
return (*Int64Array)(&a)
|
||||
case []string:
|
||||
return (*StringArray)(&a)
|
||||
|
||||
case *[]bool:
|
||||
return (*BoolArray)(a)
|
||||
case *[]float64:
|
||||
return (*Float64Array)(a)
|
||||
case *[]int64:
|
||||
return (*Int64Array)(a)
|
||||
case *[]string:
|
||||
return (*StringArray)(a)
|
||||
}
|
||||
|
||||
return GenericArray{a}
|
||||
}
|
||||
|
||||
// ArrayDelimiter may be optionally implemented by driver.Valuer or sql.Scanner
|
||||
// to override the array delimiter used by GenericArray.
|
||||
type ArrayDelimiter interface {
|
||||
// ArrayDelimiter returns the delimiter character(s) for this element's type.
|
||||
ArrayDelimiter() string
|
||||
}
|
||||
|
||||
// BoolArray represents a one-dimensional array of the PostgreSQL boolean type.
|
||||
type BoolArray []bool
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *BoolArray) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to BoolArray", src)
|
||||
}
|
||||
|
||||
func (a *BoolArray) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "BoolArray")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(BoolArray, len(elems))
|
||||
for i, v := range elems {
|
||||
if len(v) != 1 {
|
||||
return fmt.Errorf("pq: could not parse boolean array index %d: invalid boolean %q", i, v)
|
||||
}
|
||||
switch v[0] {
|
||||
case 't':
|
||||
b[i] = true
|
||||
case 'f':
|
||||
b[i] = false
|
||||
default:
|
||||
return fmt.Errorf("pq: could not parse boolean array index %d: invalid boolean %q", i, v)
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a BoolArray) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be exactly two curly brackets, N bytes of values,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 1+2*n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
b[2*i] = ','
|
||||
if a[i] {
|
||||
b[1+2*i] = 't'
|
||||
} else {
|
||||
b[1+2*i] = 'f'
|
||||
}
|
||||
}
|
||||
|
||||
b[0] = '{'
|
||||
b[2*n] = '}'
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// ByteaArray represents a one-dimensional array of the PostgreSQL bytea type.
|
||||
type ByteaArray [][]byte
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *ByteaArray) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to ByteaArray", src)
|
||||
}
|
||||
|
||||
func (a *ByteaArray) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "ByteaArray")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(ByteaArray, len(elems))
|
||||
for i, v := range elems {
|
||||
b[i], err = parseBytea(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse bytea array index %d: %s", i, err.Error())
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface. It uses the "hex" format which
|
||||
// is only supported on PostgreSQL 9.0 or newer.
|
||||
func (a ByteaArray) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be at least two curly brackets, 2*N bytes of quotes,
|
||||
// 3*N bytes of hex formatting, and N-1 bytes of delimiters.
|
||||
size := 1 + 6*n
|
||||
for _, x := range a {
|
||||
size += hex.EncodedLen(len(x))
|
||||
}
|
||||
|
||||
b := make([]byte, size)
|
||||
|
||||
for i, s := 0, b; i < n; i++ {
|
||||
o := copy(s, `,"\\x`)
|
||||
o += hex.Encode(s[o:], a[i])
|
||||
s[o] = '"'
|
||||
s = s[o+1:]
|
||||
}
|
||||
|
||||
b[0] = '{'
|
||||
b[size-1] = '}'
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// Float64Array represents a one-dimensional array of the PostgreSQL double
|
||||
// precision type.
|
||||
type Float64Array []float64
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *Float64Array) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to Float64Array", src)
|
||||
}
|
||||
|
||||
func (a *Float64Array) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "Float64Array")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(Float64Array, len(elems))
|
||||
for i, v := range elems {
|
||||
if b[i], err = strconv.ParseFloat(string(v), 64); err != nil {
|
||||
return fmt.Errorf("pq: parsing array element index %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a Float64Array) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be at least two curly brackets, N bytes of values,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 1, 1+2*n)
|
||||
b[0] = '{'
|
||||
|
||||
b = strconv.AppendFloat(b, a[0], 'f', -1, 64)
|
||||
for i := 1; i < n; i++ {
|
||||
b = append(b, ',')
|
||||
b = strconv.AppendFloat(b, a[i], 'f', -1, 64)
|
||||
}
|
||||
|
||||
return string(append(b, '}')), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// GenericArray implements the driver.Valuer and sql.Scanner interfaces for
|
||||
// an array or slice of any dimension.
|
||||
type GenericArray struct{ A interface{} }
|
||||
|
||||
func (GenericArray) evaluateDestination(rt reflect.Type) (reflect.Type, func([]byte, reflect.Value) error, string) {
|
||||
var assign func([]byte, reflect.Value) error
|
||||
var del = ","
|
||||
|
||||
// TODO calculate the assign function for other types
|
||||
// TODO repeat this section on the element type of arrays or slices (multidimensional)
|
||||
{
|
||||
if reflect.PtrTo(rt).Implements(typeSqlScanner) {
|
||||
// dest is always addressable because it is an element of a slice.
|
||||
assign = func(src []byte, dest reflect.Value) (err error) {
|
||||
ss := dest.Addr().Interface().(sql.Scanner)
|
||||
if src == nil {
|
||||
err = ss.Scan(nil)
|
||||
} else {
|
||||
err = ss.Scan(src)
|
||||
}
|
||||
return
|
||||
}
|
||||
goto FoundType
|
||||
}
|
||||
|
||||
assign = func([]byte, reflect.Value) error {
|
||||
return fmt.Errorf("pq: scanning to %s is not implemented; only sql.Scanner", rt)
|
||||
}
|
||||
}
|
||||
|
||||
FoundType:
|
||||
|
||||
if ad, ok := reflect.Zero(rt).Interface().(ArrayDelimiter); ok {
|
||||
del = ad.ArrayDelimiter()
|
||||
}
|
||||
|
||||
return rt, assign, del
|
||||
}
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a GenericArray) Scan(src interface{}) error {
|
||||
dpv := reflect.ValueOf(a.A)
|
||||
switch {
|
||||
case dpv.Kind() != reflect.Ptr:
|
||||
return fmt.Errorf("pq: destination %T is not a pointer to array or slice", a.A)
|
||||
case dpv.IsNil():
|
||||
return fmt.Errorf("pq: destination %T is nil", a.A)
|
||||
}
|
||||
|
||||
dv := dpv.Elem()
|
||||
switch dv.Kind() {
|
||||
case reflect.Slice:
|
||||
case reflect.Array:
|
||||
default:
|
||||
return fmt.Errorf("pq: destination %T is not a pointer to array or slice", a.A)
|
||||
}
|
||||
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src, dv)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src), dv)
|
||||
case nil:
|
||||
if dv.Kind() == reflect.Slice {
|
||||
dv.Set(reflect.Zero(dv.Type()))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to %s", src, dv.Type())
|
||||
}
|
||||
|
||||
func (a GenericArray) scanBytes(src []byte, dv reflect.Value) error {
|
||||
dtype, assign, del := a.evaluateDestination(dv.Type().Elem())
|
||||
dims, elems, err := parseArray(src, []byte(del))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO allow multidimensional
|
||||
|
||||
if len(dims) > 1 {
|
||||
return fmt.Errorf("pq: scanning from multidimensional ARRAY%s is not implemented",
|
||||
strings.Replace(fmt.Sprint(dims), " ", "][", -1))
|
||||
}
|
||||
|
||||
// Treat a zero-dimensional array like an array with a single dimension of zero.
|
||||
if len(dims) == 0 {
|
||||
dims = append(dims, 0)
|
||||
}
|
||||
|
||||
for i, rt := 0, dv.Type(); i < len(dims); i, rt = i+1, rt.Elem() {
|
||||
switch rt.Kind() {
|
||||
case reflect.Slice:
|
||||
case reflect.Array:
|
||||
if rt.Len() != dims[i] {
|
||||
return fmt.Errorf("pq: cannot convert ARRAY%s to %s",
|
||||
strings.Replace(fmt.Sprint(dims), " ", "][", -1), dv.Type())
|
||||
}
|
||||
default:
|
||||
// TODO handle multidimensional
|
||||
}
|
||||
}
|
||||
|
||||
values := reflect.MakeSlice(reflect.SliceOf(dtype), len(elems), len(elems))
|
||||
for i, e := range elems {
|
||||
if err := assign(e, values.Index(i)); err != nil {
|
||||
return fmt.Errorf("pq: parsing array element index %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle multidimensional
|
||||
|
||||
switch dv.Kind() {
|
||||
case reflect.Slice:
|
||||
dv.Set(values.Slice(0, dims[0]))
|
||||
case reflect.Array:
|
||||
for i := 0; i < dims[0]; i++ {
|
||||
dv.Index(i).Set(values.Index(i))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a GenericArray) Value() (driver.Value, error) {
|
||||
if a.A == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(a.A)
|
||||
|
||||
switch rv.Kind() {
|
||||
case reflect.Slice:
|
||||
if rv.IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
case reflect.Array:
|
||||
default:
|
||||
return nil, fmt.Errorf("pq: Unable to convert %T to array", a.A)
|
||||
}
|
||||
|
||||
if n := rv.Len(); n > 0 {
|
||||
// There will be at least two curly brackets, N bytes of values,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 0, 1+2*n)
|
||||
|
||||
b, _, err := appendArray(b, rv, n)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// Int64Array represents a one-dimensional array of the PostgreSQL integer types.
|
||||
type Int64Array []int64
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *Int64Array) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to Int64Array", src)
|
||||
}
|
||||
|
||||
func (a *Int64Array) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "Int64Array")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(Int64Array, len(elems))
|
||||
for i, v := range elems {
|
||||
if b[i], err = strconv.ParseInt(string(v), 10, 64); err != nil {
|
||||
return fmt.Errorf("pq: parsing array element index %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a Int64Array) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be at least two curly brackets, N bytes of values,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 1, 1+2*n)
|
||||
b[0] = '{'
|
||||
|
||||
b = strconv.AppendInt(b, a[0], 10)
|
||||
for i := 1; i < n; i++ {
|
||||
b = append(b, ',')
|
||||
b = strconv.AppendInt(b, a[i], 10)
|
||||
}
|
||||
|
||||
return string(append(b, '}')), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// StringArray represents a one-dimensional array of the PostgreSQL character types.
|
||||
type StringArray []string
|
||||
|
||||
// Scan implements the sql.Scanner interface.
|
||||
func (a *StringArray) Scan(src interface{}) error {
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
return a.scanBytes(src)
|
||||
case string:
|
||||
return a.scanBytes([]byte(src))
|
||||
case nil:
|
||||
*a = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("pq: cannot convert %T to StringArray", src)
|
||||
}
|
||||
|
||||
func (a *StringArray) scanBytes(src []byte) error {
|
||||
elems, err := scanLinearArray(src, []byte{','}, "StringArray")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *a != nil && len(elems) == 0 {
|
||||
*a = (*a)[:0]
|
||||
} else {
|
||||
b := make(StringArray, len(elems))
|
||||
for i, v := range elems {
|
||||
if b[i] = string(v); v == nil {
|
||||
return fmt.Errorf("pq: parsing array element index %d: cannot convert nil to string", i)
|
||||
}
|
||||
}
|
||||
*a = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface.
|
||||
func (a StringArray) Value() (driver.Value, error) {
|
||||
if a == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if n := len(a); n > 0 {
|
||||
// There will be at least two curly brackets, 2*N bytes of quotes,
|
||||
// and N-1 bytes of delimiters.
|
||||
b := make([]byte, 1, 1+3*n)
|
||||
b[0] = '{'
|
||||
|
||||
b = appendArrayQuotedBytes(b, []byte(a[0]))
|
||||
for i := 1; i < n; i++ {
|
||||
b = append(b, ',')
|
||||
b = appendArrayQuotedBytes(b, []byte(a[i]))
|
||||
}
|
||||
|
||||
return string(append(b, '}')), nil
|
||||
}
|
||||
|
||||
return "{}", nil
|
||||
}
|
||||
|
||||
// appendArray appends rv to the buffer, returning the extended buffer and
|
||||
// the delimiter used between elements.
|
||||
//
|
||||
// It panics when n <= 0 or rv's Kind is not reflect.Array nor reflect.Slice.
|
||||
func appendArray(b []byte, rv reflect.Value, n int) ([]byte, string, error) {
|
||||
var del string
|
||||
var err error
|
||||
|
||||
b = append(b, '{')
|
||||
|
||||
if b, del, err = appendArrayElement(b, rv.Index(0)); err != nil {
|
||||
return b, del, err
|
||||
}
|
||||
|
||||
for i := 1; i < n; i++ {
|
||||
b = append(b, del...)
|
||||
if b, del, err = appendArrayElement(b, rv.Index(i)); err != nil {
|
||||
return b, del, err
|
||||
}
|
||||
}
|
||||
|
||||
return append(b, '}'), del, nil
|
||||
}
|
||||
|
||||
// appendArrayElement appends rv to the buffer, returning the extended buffer
|
||||
// and the delimiter to use before the next element.
|
||||
//
|
||||
// When rv's Kind is neither reflect.Array nor reflect.Slice, it is converted
|
||||
// using driver.DefaultParameterConverter and the resulting []byte or string
|
||||
// is double-quoted.
|
||||
//
|
||||
// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO
|
||||
func appendArrayElement(b []byte, rv reflect.Value) ([]byte, string, error) {
|
||||
if k := rv.Kind(); k == reflect.Array || k == reflect.Slice {
|
||||
if t := rv.Type(); t != typeByteSlice && !t.Implements(typeDriverValuer) {
|
||||
if n := rv.Len(); n > 0 {
|
||||
return appendArray(b, rv, n)
|
||||
}
|
||||
|
||||
return b, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
var del string = ","
|
||||
var err error
|
||||
var iv interface{} = rv.Interface()
|
||||
|
||||
if ad, ok := iv.(ArrayDelimiter); ok {
|
||||
del = ad.ArrayDelimiter()
|
||||
}
|
||||
|
||||
if iv, err = driver.DefaultParameterConverter.ConvertValue(iv); err != nil {
|
||||
return b, del, err
|
||||
}
|
||||
|
||||
switch v := iv.(type) {
|
||||
case nil:
|
||||
return append(b, "NULL"...), del, nil
|
||||
case []byte:
|
||||
return appendArrayQuotedBytes(b, v), del, nil
|
||||
case string:
|
||||
return appendArrayQuotedBytes(b, []byte(v)), del, nil
|
||||
}
|
||||
|
||||
b, err = appendValue(b, iv)
|
||||
return b, del, err
|
||||
}
|
||||
|
||||
func appendArrayQuotedBytes(b, v []byte) []byte {
|
||||
b = append(b, '"')
|
||||
for {
|
||||
i := bytes.IndexAny(v, `"\`)
|
||||
if i < 0 {
|
||||
b = append(b, v...)
|
||||
break
|
||||
}
|
||||
if i > 0 {
|
||||
b = append(b, v[:i]...)
|
||||
}
|
||||
b = append(b, '\\', v[i])
|
||||
v = v[i+1:]
|
||||
}
|
||||
return append(b, '"')
|
||||
}
|
||||
|
||||
func appendValue(b []byte, v driver.Value) ([]byte, error) {
|
||||
return append(b, encode(nil, v, 0)...), nil
|
||||
}
|
||||
|
||||
// parseArray extracts the dimensions and elements of an array represented in
|
||||
// text format. Only representations emitted by the backend are supported.
|
||||
// Notably, whitespace around brackets and delimiters is significant, and NULL
|
||||
// is case-sensitive.
|
||||
//
|
||||
// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO
|
||||
func parseArray(src, del []byte) (dims []int, elems [][]byte, err error) {
|
||||
var depth, i int
|
||||
|
||||
if len(src) < 1 || src[0] != '{' {
|
||||
return nil, nil, fmt.Errorf("pq: unable to parse array; expected %q at offset %d", '{', 0)
|
||||
}
|
||||
|
||||
Open:
|
||||
for i < len(src) {
|
||||
switch src[i] {
|
||||
case '{':
|
||||
depth++
|
||||
i++
|
||||
case '}':
|
||||
elems = make([][]byte, 0)
|
||||
goto Close
|
||||
default:
|
||||
break Open
|
||||
}
|
||||
}
|
||||
dims = make([]int, i)
|
||||
|
||||
Element:
|
||||
for i < len(src) {
|
||||
switch src[i] {
|
||||
case '{':
|
||||
if depth == len(dims) {
|
||||
break Element
|
||||
}
|
||||
depth++
|
||||
dims[depth-1] = 0
|
||||
i++
|
||||
case '"':
|
||||
var elem = []byte{}
|
||||
var escape bool
|
||||
for i++; i < len(src); i++ {
|
||||
if escape {
|
||||
elem = append(elem, src[i])
|
||||
escape = false
|
||||
} else {
|
||||
switch src[i] {
|
||||
default:
|
||||
elem = append(elem, src[i])
|
||||
case '\\':
|
||||
escape = true
|
||||
case '"':
|
||||
elems = append(elems, elem)
|
||||
i++
|
||||
break Element
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
for start := i; i < len(src); i++ {
|
||||
if bytes.HasPrefix(src[i:], del) || src[i] == '}' {
|
||||
elem := src[start:i]
|
||||
if len(elem) == 0 {
|
||||
return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i)
|
||||
}
|
||||
if bytes.Equal(elem, []byte("NULL")) {
|
||||
elem = nil
|
||||
}
|
||||
elems = append(elems, elem)
|
||||
break Element
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i < len(src) {
|
||||
if bytes.HasPrefix(src[i:], del) && depth > 0 {
|
||||
dims[depth-1]++
|
||||
i += len(del)
|
||||
goto Element
|
||||
} else if src[i] == '}' && depth > 0 {
|
||||
dims[depth-1]++
|
||||
depth--
|
||||
i++
|
||||
} else {
|
||||
return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i)
|
||||
}
|
||||
}
|
||||
|
||||
Close:
|
||||
for i < len(src) {
|
||||
if src[i] == '}' && depth > 0 {
|
||||
depth--
|
||||
i++
|
||||
} else {
|
||||
return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i)
|
||||
}
|
||||
}
|
||||
if depth > 0 {
|
||||
err = fmt.Errorf("pq: unable to parse array; expected %q at offset %d", '}', i)
|
||||
}
|
||||
if err == nil {
|
||||
for _, d := range dims {
|
||||
if (len(elems) % d) != 0 {
|
||||
err = fmt.Errorf("pq: multidimensional arrays must have elements with matching dimensions")
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func scanLinearArray(src, del []byte, typ string) (elems [][]byte, err error) {
|
||||
dims, elems, err := parseArray(src, del)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dims) > 1 {
|
||||
return nil, fmt.Errorf("pq: cannot convert ARRAY%s to %s", strings.Replace(fmt.Sprint(dims), " ", "][", -1), typ)
|
||||
}
|
||||
return elems, err
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user