Merge remote-tracking branch 'upstream/master'

This commit is contained in:
pengpengshui 2017-07-13 17:14:31 -07:00
commit be6e265890
121 changed files with 8933 additions and 326 deletions

View File

@ -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

View File

@ -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 (

View File

@ -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);

View File

@ -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

View 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"]

View File

@ -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

View File

@ -66,4 +66,6 @@ const (
WithNotary = "with_notary"
WithClair = "with_clair"
ScanAllPolicy = "scan_all_policy"
DefaultClairEndpoint = "http://clair:6060"
)

View File

@ -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)

View 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
}

View File

@ -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
View 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)
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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"`

View 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
}

View 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,
})
}

View 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)
}
}

View 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
}

View File

@ -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")
}
}

View 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
)

View 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
}

View 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)
}
}

View 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
}

View 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)
}

View 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)
}
}

View 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()
}
}

View 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")
}
}

View 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()
}

View 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()
}
}

View 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
}

View 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
}

View File

@ -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{}

View File

@ -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{},
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -18,7 +18,7 @@ import (
)
func TestError(t *testing.T) {
err := &Error{
err := &HTTPError{
StatusCode: 404,
Detail: "not found",
}

View File

@ -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

View File

@ -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

View File

@ -78,7 +78,7 @@ func getToken(client *http.Client, credential Credential, realm, service string,
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, &registry_error.Error{
return nil, &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(data),
}

View File

@ -126,7 +126,7 @@ func (r *Registry) Catalog() ([]string, error) {
suffix = ""
}
} else {
return repos, &registry_error.Error{
return repos, &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}
@ -157,7 +157,7 @@ func (r *Registry) Ping() error {
return err
}
return &registry_error.Error{
return &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}

View File

@ -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, &registry_error.Error{
return tags, &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}
@ -160,7 +160,7 @@ func (r *Repository) ManifestExist(reference string) (digest string, exist bool,
return
}
err = &registry_error.Error{
err = &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}
@ -197,7 +197,7 @@ func (r *Repository) PullManifest(reference string, acceptMediaTypes []string) (
return
}
err = &registry_error.Error{
err = &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}
@ -232,7 +232,7 @@ func (r *Repository) PushManifest(reference, mediaType string, payload []byte) (
return
}
err = &registry_error.Error{
err = &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}
@ -263,7 +263,7 @@ func (r *Repository) DeleteManifest(digest string) error {
return err
}
return &registry_error.Error{
return &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}
@ -277,7 +277,7 @@ func (r *Repository) DeleteTag(tag string) error {
}
if !exist {
return &registry_error.Error{
return &registry_error.HTTPError{
StatusCode: http.StatusNotFound,
}
}
@ -312,7 +312,7 @@ func (r *Repository) BlobExist(digest string) (bool, error) {
return false, err
}
return false, &registry_error.Error{
return false, &registry_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 = &registry_error.Error{
err = &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}
@ -379,7 +379,7 @@ func (r *Repository) initiateBlobUpload(name string) (location, uploadUUID strin
return
}
err = &registry_error.Error{
err = &registry_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 &registry_error.Error{
return &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}
@ -447,7 +447,7 @@ func (r *Repository) DeleteBlob(digest string) error {
return err
}
return &registry_error.Error{
return &registry_error.HTTPError{
StatusCode: resp.StatusCode,
Detail: string(b),
}

View File

@ -396,10 +396,10 @@ func TestListTag(t *testing.T) {
func TestParseError(t *testing.T) {
err := &url.Error{
Err: &registry_error.Error{},
Err: &registry_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")
}
}

View 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
}

View 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)
}

View File

@ -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 {

View File

@ -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()
}
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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.

View File

@ -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, &notifier.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)

View File

@ -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),
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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", &registry.NotificationHandler{})

View File

@ -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)

View File

@ -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{

View File

@ -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 {

View File

@ -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 }
})
...
```

View 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
});
};

View File

@ -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"
}
}
}

View File

@ -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);

View File

@ -0,0 +1,9 @@
export const REGISTRY_CONFIG_STYLES: string = `
.info-tips-icon {
color: grey;
}
.info-tips-icon:hover {
color: #007CBB;
}
`;

View File

@ -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>
`;

View File

@ -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();
});
}));

View File

@ -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));
}
}

View File

@ -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">

View File

@ -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 {

View File

@ -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>
`;

View File

@ -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;
}
}

View File

@ -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;
}
`;

View File

@ -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(() => {

View File

@ -3,7 +3,6 @@ export const REPOSITORY_STACKVIEW_STYLES: string = `
padding-right: 16px;
}
.sub-grid-custom {
position: relative;
left: 40px;
}
.refresh-btn {

View File

@ -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 {

View File

@ -5,6 +5,7 @@ export const TAG_STYLE = `
.embeded-datagrid {
width: 98%;
float:right; /*add for issue #2688*/
}
.hidden-tag {

View File

@ -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();
}
}
}
}

View File

@ -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"
}
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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.",

View File

@ -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.",

View File

@ -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服务器的连通正常。",

View File

@ -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
View 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
View 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
View 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
View 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