mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-03 14:37:44 +01:00
Merge remote-tracking branch 'upstream/master' into 170608_project
This commit is contained in:
commit
20cf8de0f7
@ -232,8 +232,8 @@ paths:
|
|||||||
description: Project ID does not exist.
|
description: Project ID does not exist.
|
||||||
500:
|
500:
|
||||||
description: Unexpected internal errors.
|
description: Unexpected internal errors.
|
||||||
/projects/{project_id}/logs/filter:
|
/projects/{project_id}/logs:
|
||||||
post:
|
get:
|
||||||
summary: Get access logs accompany with a relevant project.
|
summary: Get access logs accompany with a relevant project.
|
||||||
description: |
|
description: |
|
||||||
This endpoint let user search access logs filtered by operations and date time ranges.
|
This endpoint let user search access logs filtered by operations and date time ranges.
|
||||||
@ -244,11 +244,36 @@ paths:
|
|||||||
format: int64
|
format: int64
|
||||||
required: true
|
required: true
|
||||||
description: Relevant project ID
|
description: Relevant project ID
|
||||||
- name: access_log
|
- name: username
|
||||||
in: body
|
in: query
|
||||||
schema:
|
type: string
|
||||||
$ref: '#/definitions/AccessLogFilter'
|
required: false
|
||||||
description: Search results of access logs.
|
description: Username of the operator.
|
||||||
|
- name: repository
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The name of repository
|
||||||
|
- name: tag
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The name of tag
|
||||||
|
- name: operation
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The operation
|
||||||
|
- name: begin_timestamp
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The begin timestamp
|
||||||
|
- name: end_timestamp
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The end timestamp
|
||||||
- name: page
|
- name: page
|
||||||
in: query
|
in: query
|
||||||
type: integer
|
type: integer
|
||||||
@ -876,6 +901,36 @@ paths:
|
|||||||
description: |
|
description: |
|
||||||
This endpoint let user see the recent operation logs of the projects which he is member of
|
This endpoint let user see the recent operation logs of the projects which he is member of
|
||||||
parameters:
|
parameters:
|
||||||
|
- name: username
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: Username of the operator.
|
||||||
|
- name: repository
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The name of repository
|
||||||
|
- name: tag
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The name of tag
|
||||||
|
- name: operation
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The operation
|
||||||
|
- name: begin_timestamp
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The begin timestamp
|
||||||
|
- name: end_timestamp
|
||||||
|
in: query
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: The end timestamp
|
||||||
- name: page
|
- name: page
|
||||||
in: query
|
in: query
|
||||||
type: integer
|
type: integer
|
||||||
|
@ -67,13 +67,19 @@ func logQueryConditions(query *models.LogQueryParam) orm.QuerySeter {
|
|||||||
qs = qs.Filter("username__contains", query.Username)
|
qs = qs.Filter("username__contains", query.Username)
|
||||||
}
|
}
|
||||||
if len(query.Repository) != 0 {
|
if len(query.Repository) != 0 {
|
||||||
qs = qs.Filter("repo_name", query.Repository)
|
qs = qs.Filter("repo_name__contains", query.Repository)
|
||||||
}
|
}
|
||||||
if len(query.Tag) != 0 {
|
if len(query.Tag) != 0 {
|
||||||
qs = qs.Filter("repo_tag", query.Tag)
|
qs = qs.Filter("repo_tag__contains", query.Tag)
|
||||||
}
|
}
|
||||||
if len(query.Operations) > 0 {
|
operations := []string{}
|
||||||
qs = qs.Filter("operation__in", query.Operations)
|
for _, operation := range query.Operations {
|
||||||
|
if len(operation) > 0 {
|
||||||
|
operations = append(operations, operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(operations) > 0 {
|
||||||
|
qs = qs.Filter("operation__in", operations)
|
||||||
}
|
}
|
||||||
if query.BeginTime != nil {
|
if query.BeginTime != nil {
|
||||||
qs = qs.Filter("op_time__gte", query.BeginTime)
|
qs = qs.Filter("op_time__gte", query.BeginTime)
|
||||||
|
@ -19,19 +19,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// AccessLog holds information about logs which are used to record the actions that user take to the resourses.
|
// AccessLog holds information about logs which are used to record the actions that user take to the resourses.
|
||||||
// TODO remove useless attrs
|
|
||||||
type AccessLog struct {
|
type AccessLog struct {
|
||||||
LogID int `orm:"pk;auto;column(log_id)" json:"log_id"`
|
LogID int `orm:"pk;auto;column(log_id)" json:"log_id"`
|
||||||
Username string `orm:"column(username)" json:"username"`
|
Username string `orm:"column(username)" json:"username"`
|
||||||
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
|
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
|
||||||
RepoName string `orm:"column(repo_name)" json:"repo_name"`
|
RepoName string `orm:"column(repo_name)" json:"repo_name"`
|
||||||
RepoTag string `orm:"column(repo_tag)" json:"repo_tag"`
|
RepoTag string `orm:"column(repo_tag)" json:"repo_tag"`
|
||||||
GUID string `orm:"column(GUID)" json:"guid"`
|
GUID string `orm:"column(GUID)" json:"guid"`
|
||||||
Operation string `orm:"column(operation)" json:"operation"`
|
Operation string `orm:"column(operation)" json:"operation"`
|
||||||
OpTime time.Time `orm:"column(op_time)" json:"op_time"`
|
OpTime time.Time `orm:"column(op_time)" json:"op_time"`
|
||||||
Keywords string `orm:"-" json:"keywords"`
|
|
||||||
BeginTimestamp int64 `orm:"-" json:"begin_timestamp"`
|
|
||||||
EndTimestamp int64 `orm:"-" json:"end_timestamp"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogQueryParam is used to set query conditions when listing
|
// LogQueryParam is used to set query conditions when listing
|
||||||
|
59
src/common/models/clair.go
Normal file
59
src/common/models/clair.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
//ClairLayer ...
|
||||||
|
type ClairLayer struct {
|
||||||
|
Name string `json:"Name,omitempty"`
|
||||||
|
NamespaceNames []string `json:"NamespaceNames,omitempty"`
|
||||||
|
Path string `json:"Path,omitempty"`
|
||||||
|
Headers map[string]string `json:"Headers,omitempty"`
|
||||||
|
ParentName string `json:"ParentName,omitempty"`
|
||||||
|
Format string `json:"Format,omitempty"`
|
||||||
|
Features []ClairFeature `json:"Features,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//ClairFeature ...
|
||||||
|
type ClairFeature struct {
|
||||||
|
Name string `json:"Name,omitempty"`
|
||||||
|
NamespaceName string `json:"NamespaceName,omitempty"`
|
||||||
|
VersionFormat string `json:"VersionFormat,omitempty"`
|
||||||
|
Version string `json:"Version,omitempty"`
|
||||||
|
Vulnerabilities []ClairVulnerability `json:"Vulnerabilities,omitempty"`
|
||||||
|
AddedBy string `json:"AddedBy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//ClairVulnerability ...
|
||||||
|
type ClairVulnerability struct {
|
||||||
|
Name string `json:"Name,omitempty"`
|
||||||
|
NamespaceName string `json:"NamespaceName,omitempty"`
|
||||||
|
Description string `json:"Description,omitempty"`
|
||||||
|
Link string `json:"Link,omitempty"`
|
||||||
|
Severity string `json:"Severity,omitempty"`
|
||||||
|
Metadata map[string]interface{} `json:"Metadata,omitempty"`
|
||||||
|
FixedBy string `json:"FixedBy,omitempty"`
|
||||||
|
FixedIn []ClairFeature `json:"FixedIn,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//ClairError ...
|
||||||
|
type ClairError struct {
|
||||||
|
Message string `json:"Message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//ClairLayerEnvelope ...
|
||||||
|
type ClairLayerEnvelope struct {
|
||||||
|
Layer *ClairLayer `json:"Layer,omitempty"`
|
||||||
|
Error *ClairError `json:"Error,omitempty"`
|
||||||
|
}
|
@ -19,6 +19,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -114,3 +115,13 @@ func TestTCPConn(addr string, timeout, interval int) error {
|
|||||||
return fmt.Errorf("failed to connect to tcp:%s after %d seconds", addr, timeout)
|
return fmt.Errorf("failed to connect to tcp:%s after %d seconds", addr, timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseTimeStamp parse timestamp to time
|
||||||
|
func ParseTimeStamp(timestamp string) (*time.Time, error) {
|
||||||
|
i, err := strconv.ParseInt(timestamp, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t := time.Unix(i, 0)
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
@ -17,8 +17,12 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseEndpoint(t *testing.T) {
|
func TestParseEndpoint(t *testing.T) {
|
||||||
@ -191,3 +195,19 @@ func TestTestTCPConn(t *testing.T) {
|
|||||||
t.Fatalf("failed to test tcp connection of %s: %v", addr, err)
|
t.Fatalf("failed to test tcp connection of %s: %v", addr, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseTimeStamp(t *testing.T) {
|
||||||
|
// invalid input
|
||||||
|
_, err := ParseTimeStamp("")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
// invalid input
|
||||||
|
_, err = ParseTimeStamp("invalid")
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
// valid
|
||||||
|
now := time.Now().Unix()
|
||||||
|
result, err := ParseTimeStamp(strconv.FormatInt(now, 10))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, now, result.Unix())
|
||||||
|
}
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/vmware/harbor/src/common/utils/log"
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
"github.com/vmware/harbor/src/common/utils/registry/auth"
|
||||||
"github.com/vmware/harbor/src/jobservice/config"
|
"github.com/vmware/harbor/src/jobservice/config"
|
||||||
|
"github.com/vmware/harbor/src/jobservice/job"
|
||||||
"github.com/vmware/harbor/src/jobservice/utils"
|
"github.com/vmware/harbor/src/jobservice/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -83,5 +84,8 @@ func (isj *ImageScanJob) Post() {
|
|||||||
isj.RenderError(http.StatusInternalServerError, "Failed to insert scan job data.")
|
isj.RenderError(http.StatusInternalServerError, "Failed to insert scan job data.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Debugf("job id: %d", jid)
|
log.Debugf("Scan job id: %d", jid)
|
||||||
|
sj := job.NewScanJob(jid)
|
||||||
|
log.Debugf("Sent job to scheduler, job: %v", sj)
|
||||||
|
job.Schedule(sj)
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ func TestScanJob(t *testing.T) {
|
|||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
j, err := dao.GetScanJob(scanJobID)
|
j, err := dao.GetScanJob(scanJobID)
|
||||||
assert.Equal(models.JobRetrying, j.Status)
|
assert.Equal(models.JobRetrying, j.Status)
|
||||||
assert.Equal("sha256:0204dc6e09fa57ab99ac40e415eb637d62c8b2571ecbbc9ca0eb5e2ad2b5c56f", sj.parm.digest)
|
assert.Equal("sha256:0204dc6e09fa57ab99ac40e415eb637d62c8b2571ecbbc9ca0eb5e2ad2b5c56f", sj.parm.Digest)
|
||||||
sj2 := NewScanJob(99999)
|
sj2 := NewScanJob(99999)
|
||||||
err = sj2.Init()
|
err = sj2.Init()
|
||||||
assert.NotNil(err)
|
assert.NotNil(err)
|
||||||
|
@ -176,9 +176,9 @@ type ScanJob struct {
|
|||||||
|
|
||||||
//ScanJobParm wraps the parms of a image scan job.
|
//ScanJobParm wraps the parms of a image scan job.
|
||||||
type ScanJobParm struct {
|
type ScanJobParm struct {
|
||||||
repository string
|
Repository string
|
||||||
tag string
|
Tag string
|
||||||
digest string
|
Digest string
|
||||||
}
|
}
|
||||||
|
|
||||||
//ID returns the id of the scan
|
//ID returns the id of the scan
|
||||||
@ -216,9 +216,9 @@ func (sj *ScanJob) Init() error {
|
|||||||
return fmt.Errorf("The job doesn't exist in DB, job id: %d", sj.id)
|
return fmt.Errorf("The job doesn't exist in DB, job id: %d", sj.id)
|
||||||
}
|
}
|
||||||
sj.parm = &ScanJobParm{
|
sj.parm = &ScanJobParm{
|
||||||
repository: job.Repository,
|
Repository: job.Repository,
|
||||||
tag: job.Tag,
|
Tag: job.Tag,
|
||||||
digest: job.Digest,
|
Digest: job.Digest,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/vmware/harbor/src/common/utils/log"
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
"github.com/vmware/harbor/src/jobservice/config"
|
"github.com/vmware/harbor/src/jobservice/config"
|
||||||
"github.com/vmware/harbor/src/jobservice/replication"
|
"github.com/vmware/harbor/src/jobservice/replication"
|
||||||
|
"github.com/vmware/harbor/src/jobservice/scan"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SM is the state machine to handle job, it handles one job at a time.
|
// SM is the state machine to handle job, it handles one job at a time.
|
||||||
@ -231,7 +232,12 @@ func (sm *SM) initTransitions() error {
|
|||||||
return fmt.Errorf("unsupported operation: %s", jobParm.Operation)
|
return fmt.Errorf("unsupported operation: %s", jobParm.Operation)
|
||||||
}
|
}
|
||||||
case ScanType:
|
case ScanType:
|
||||||
log.Debugf("TODO for scan job, job: %v", sm.CurrentJob)
|
scanJob, ok := sm.CurrentJob.(*ScanJob)
|
||||||
|
if !ok {
|
||||||
|
//Shouldn't be here.
|
||||||
|
return fmt.Errorf("The job: %v is not a type of ScanJob", sm.CurrentJob)
|
||||||
|
}
|
||||||
|
addImgScanTransition(sm, scanJob.parm)
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Unsupported job type: %v", sm.CurrentJob.Type())
|
return fmt.Errorf("Unsupported job type: %v", sm.CurrentJob.Type())
|
||||||
@ -247,6 +253,20 @@ func addTestTransition(sm *SM) error {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
func addImgScanTransition(sm *SM, parm *ScanJobParm) {
|
||||||
|
ctx := &scan.JobContext{
|
||||||
|
Repository: parm.Repository,
|
||||||
|
Tag: parm.Tag,
|
||||||
|
Digest: parm.Digest,
|
||||||
|
Logger: sm.Logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.AddTransition(models.JobRunning, scan.StateInitialize, &scan.Initializer{Context: ctx})
|
||||||
|
sm.AddTransition(scan.StateInitialize, scan.StateScanLayer, &scan.LayerScanHandler{Context: ctx})
|
||||||
|
sm.AddTransition(scan.StateScanLayer, scan.StateSummarize, &scan.SummarizeHandler{Context: ctx})
|
||||||
|
sm.AddTransition(scan.StateSummarize, models.JobFinished, &StatusUpdater{sm.CurrentJob, models.JobFinished})
|
||||||
|
}
|
||||||
|
|
||||||
func addImgTransferTransition(sm *SM, parm *RepJobParm) {
|
func addImgTransferTransition(sm *SM, parm *RepJobParm) {
|
||||||
base := replication.InitBaseHandler(parm.Repository, parm.LocalRegURL, config.JobserviceSecret(),
|
base := replication.InitBaseHandler(parm.Repository, parm.LocalRegURL, config.JobserviceSecret(),
|
||||||
parm.TargetURL, parm.TargetUsername, parm.TargetPassword,
|
parm.TargetURL, parm.TargetUsername, parm.TargetPassword,
|
||||||
|
44
src/jobservice/scan/context.go
Normal file
44
src/jobservice/scan/context.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// 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 scan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// StateInitialize in this state the handler will initialize the job context.
|
||||||
|
StateInitialize = "initialize"
|
||||||
|
// StateScanLayer in this state the handler will POST layer of clair to scan layer by layer of the image.
|
||||||
|
StateScanLayer = "scanlayer"
|
||||||
|
// StateSummarize in this state, the layers are scanned by clair it will call clair API to update vulnerability overview in Harbor DB. After this state, the job is finished.
|
||||||
|
StateSummarize = "summarize"
|
||||||
|
)
|
||||||
|
|
||||||
|
//JobContext is for sharing data across handlers in a execution of a scan job.
|
||||||
|
type JobContext struct {
|
||||||
|
Repository string
|
||||||
|
Tag string
|
||||||
|
Digest string
|
||||||
|
//the digests of layers
|
||||||
|
layers []string
|
||||||
|
//each layer name has to be unique, so it should be ${img-digest}-${layer-digest}
|
||||||
|
layerNames []string
|
||||||
|
//the index of current layer
|
||||||
|
current int
|
||||||
|
//token for accessing the registry
|
||||||
|
token string
|
||||||
|
Logger *log.Logger
|
||||||
|
}
|
70
src/jobservice/scan/handlers.go
Normal file
70
src/jobservice/scan/handlers.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// 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 scan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vmware/harbor/src/common/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initializer will handle the initialise state pull the manifest, prepare token.
|
||||||
|
type Initializer struct {
|
||||||
|
Context *JobContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter ...
|
||||||
|
func (iz *Initializer) Enter() (string, error) {
|
||||||
|
logger := iz.Context.Logger
|
||||||
|
logger.Infof("Entered scan initializer")
|
||||||
|
return StateScanLayer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit ...
|
||||||
|
func (iz *Initializer) Exit() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//LayerScanHandler will call clair API to trigger scanning.
|
||||||
|
type LayerScanHandler struct {
|
||||||
|
Context *JobContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter ...
|
||||||
|
func (ls *LayerScanHandler) Enter() (string, error) {
|
||||||
|
logger := ls.Context.Logger
|
||||||
|
logger.Infof("Entered scan layer handler")
|
||||||
|
return StateSummarize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit ...
|
||||||
|
func (ls *LayerScanHandler) Exit() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SummarizeHandler will summarize the vulnerability and feature information of Clair, and store into Harbor's DB.
|
||||||
|
type SummarizeHandler struct {
|
||||||
|
Context *JobContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter ...
|
||||||
|
func (sh *SummarizeHandler) Enter() (string, error) {
|
||||||
|
logger := sh.Context.Logger
|
||||||
|
logger.Infof("Entered summarize handler")
|
||||||
|
return models.JobFinished, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit ...
|
||||||
|
func (sh *SummarizeHandler) Exit() error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -96,7 +96,7 @@ func init() {
|
|||||||
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
|
beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword")
|
||||||
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
|
beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole")
|
||||||
beego.Router("/api/projects/:id/publicity", &ProjectAPI{}, "put:ToggleProjectPublic")
|
beego.Router("/api/projects/:id/publicity", &ProjectAPI{}, "put:ToggleProjectPublic")
|
||||||
beego.Router("/api/projects/:id([0-9]+)/logs/filter", &ProjectAPI{}, "post:FilterAccessLog")
|
beego.Router("/api/projects/:id([0-9]+)/logs", &ProjectAPI{}, "get:Logs")
|
||||||
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &ProjectMemberAPI{}, "get:Get;post:Post;delete:Delete;put:Put")
|
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &ProjectMemberAPI{}, "get:Get;post:Post;delete:Delete;put:Put")
|
||||||
beego.Router("/api/repositories", &RepositoryAPI{})
|
beego.Router("/api/repositories", &RepositoryAPI{})
|
||||||
beego.Router("/api/statistics", &StatisticAPI{})
|
beego.Router("/api/statistics", &StatisticAPI{})
|
||||||
@ -373,27 +373,12 @@ func (a testapi) ToggleProjectPublicity(prjUsr usrInfo, projectID string, ispubl
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Get access logs accompany with a relevant project.
|
//Get access logs accompany with a relevant project.
|
||||||
func (a testapi) ProjectLogsFilter(prjUsr usrInfo, projectID string, accessLog apilib.AccessLogFilter) (int, []byte, error) {
|
func (a testapi) ProjectLogs(prjUsr usrInfo, projectID string, query *apilib.LogQuery) (int, []byte, error) {
|
||||||
//func (a testapi) ProjectLogsFilter(prjUsr usrInfo, projectID string, accessLog apilib.AccessLog) (int, apilib.AccessLog, error) {
|
_sling := sling.New().Get(a.basePath).
|
||||||
_sling := sling.New().Post(a.basePath)
|
Path("/api/projects/" + projectID + "/logs").
|
||||||
|
QueryStruct(query)
|
||||||
|
|
||||||
path := "/api/projects/" + projectID + "/logs/filter"
|
return request(_sling, jsonAcceptHeader, prjUsr)
|
||||||
|
|
||||||
_sling = _sling.Path(path)
|
|
||||||
|
|
||||||
// body params
|
|
||||||
_sling = _sling.BodyJSON(accessLog)
|
|
||||||
|
|
||||||
//var successPayload []apilib.AccessLog
|
|
||||||
|
|
||||||
httpStatusCode, body, err := request(_sling, jsonAcceptHeader, prjUsr)
|
|
||||||
/*
|
|
||||||
if err == nil && httpStatusCode == 200 {
|
|
||||||
err = json.Unmarshal(body, &successPayload)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
return httpStatusCode, body, err
|
|
||||||
// return httpStatusCode, successPayload, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//-------------------------Member Test---------------------------------------//
|
//-------------------------Member Test---------------------------------------//
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
"github.com/vmware/harbor/src/common/dao"
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
"github.com/vmware/harbor/src/common/models"
|
"github.com/vmware/harbor/src/common/models"
|
||||||
|
"github.com/vmware/harbor/src/common/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
//LogAPI handles request api/logs
|
//LogAPI handles request api/logs
|
||||||
@ -43,12 +44,36 @@ func (l *LogAPI) Prepare() {
|
|||||||
func (l *LogAPI) Get() {
|
func (l *LogAPI) Get() {
|
||||||
page, size := l.GetPaginationParams()
|
page, size := l.GetPaginationParams()
|
||||||
query := &models.LogQueryParam{
|
query := &models.LogQueryParam{
|
||||||
|
Username: l.GetString("username"),
|
||||||
|
Repository: l.GetString("repository"),
|
||||||
|
Tag: l.GetString("tag"),
|
||||||
|
Operations: l.GetStrings("operation"),
|
||||||
Pagination: &models.Pagination{
|
Pagination: &models.Pagination{
|
||||||
Page: page,
|
Page: page,
|
||||||
Size: size,
|
Size: size,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
timestamp := l.GetString("begin_timestamp")
|
||||||
|
if len(timestamp) > 0 {
|
||||||
|
t, err := utils.ParseTimeStamp(timestamp)
|
||||||
|
if err != nil {
|
||||||
|
l.HandleBadRequest(fmt.Sprintf("invalid begin_timestamp: %s", timestamp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.BeginTime = t
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp = l.GetString("end_timestamp")
|
||||||
|
if len(timestamp) > 0 {
|
||||||
|
t, err := utils.ParseTimeStamp(timestamp)
|
||||||
|
if err != nil {
|
||||||
|
l.HandleBadRequest(fmt.Sprintf("invalid end_timestamp: %s", timestamp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.EndTime = t
|
||||||
|
}
|
||||||
|
|
||||||
if !l.isSysAdmin {
|
if !l.isSysAdmin {
|
||||||
projects, err := l.ProjectMgr.GetByMember(l.username)
|
projects, err := l.ProjectMgr.GetByMember(l.username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -18,11 +18,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/vmware/harbor/src/common"
|
"github.com/vmware/harbor/src/common"
|
||||||
"github.com/vmware/harbor/src/common/dao"
|
"github.com/vmware/harbor/src/common/dao"
|
||||||
"github.com/vmware/harbor/src/common/models"
|
"github.com/vmware/harbor/src/common/models"
|
||||||
|
"github.com/vmware/harbor/src/common/utils"
|
||||||
"github.com/vmware/harbor/src/common/utils/log"
|
"github.com/vmware/harbor/src/common/utils/log"
|
||||||
"github.com/vmware/harbor/src/ui/config"
|
"github.com/vmware/harbor/src/ui/config"
|
||||||
|
|
||||||
@ -391,8 +391,8 @@ func (p *ProjectAPI) ToggleProjectPublic() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterAccessLog handles GET to /api/projects/{}/logs
|
// Logs ...
|
||||||
func (p *ProjectAPI) FilterAccessLog() {
|
func (p *ProjectAPI) Logs() {
|
||||||
if !p.SecurityCtx.IsAuthenticated() {
|
if !p.SecurityCtx.IsAuthenticated() {
|
||||||
p.HandleUnauthorized()
|
p.HandleUnauthorized()
|
||||||
return
|
return
|
||||||
@ -403,51 +403,54 @@ func (p *ProjectAPI) FilterAccessLog() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var query models.AccessLog
|
page, size := p.GetPaginationParams()
|
||||||
p.DecodeJSONReq(&query)
|
query := &models.LogQueryParam{
|
||||||
|
|
||||||
queryParm := &models.LogQueryParam{
|
|
||||||
ProjectIDs: []int64{p.project.ProjectID},
|
ProjectIDs: []int64{p.project.ProjectID},
|
||||||
Username: query.Username,
|
Username: p.GetString("username"),
|
||||||
Repository: query.RepoName,
|
Repository: p.GetString("repository"),
|
||||||
Tag: query.RepoTag,
|
Tag: p.GetString("tag"),
|
||||||
|
Operations: p.GetStrings("operation"),
|
||||||
|
Pagination: &models.Pagination{
|
||||||
|
Page: page,
|
||||||
|
Size: size,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(query.Keywords) > 0 {
|
timestamp := p.GetString("begin_timestamp")
|
||||||
queryParm.Operations = strings.Split(query.Keywords, "/")
|
if len(timestamp) > 0 {
|
||||||
|
t, err := utils.ParseTimeStamp(timestamp)
|
||||||
|
if err != nil {
|
||||||
|
p.HandleBadRequest(fmt.Sprintf("invalid begin_timestamp: %s", timestamp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.BeginTime = t
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.BeginTimestamp > 0 {
|
timestamp = p.GetString("end_timestamp")
|
||||||
beginTime := time.Unix(query.BeginTimestamp, 0)
|
if len(timestamp) > 0 {
|
||||||
queryParm.BeginTime = &beginTime
|
t, err := utils.ParseTimeStamp(timestamp)
|
||||||
|
if err != nil {
|
||||||
|
p.HandleBadRequest(fmt.Sprintf("invalid end_timestamp: %s", timestamp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.EndTime = t
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.EndTimestamp > 0 {
|
total, err := dao.GetTotalOfAccessLogs(query)
|
||||||
endTime := time.Unix(query.EndTimestamp, 0)
|
|
||||||
queryParm.EndTime = &endTime
|
|
||||||
}
|
|
||||||
|
|
||||||
page, pageSize := p.GetPaginationParams()
|
|
||||||
queryParm.Pagination = &models.Pagination{
|
|
||||||
Page: page,
|
|
||||||
Size: pageSize,
|
|
||||||
}
|
|
||||||
|
|
||||||
total, err := dao.GetTotalOfAccessLogs(queryParm)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.HandleInternalServerError(fmt.Sprintf(
|
p.HandleInternalServerError(fmt.Sprintf(
|
||||||
"failed to get total of access log: %v", err))
|
"failed to get total of access log: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logs, err := dao.GetAccessLogs(queryParm)
|
logs, err := dao.GetAccessLogs(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.HandleInternalServerError(fmt.Sprintf(
|
p.HandleInternalServerError(fmt.Sprintf(
|
||||||
"failed to get access log: %v", err))
|
"failed to get access log: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.SetPaginationHeader(total, page, pageSize)
|
p.SetPaginationHeader(total, page, size)
|
||||||
p.Data["json"] = logs
|
p.Data["json"] = logs
|
||||||
p.ServeJSON()
|
p.ServeJSON()
|
||||||
}
|
}
|
||||||
|
@ -334,19 +334,19 @@ func TestProjectLogsFilter(t *testing.T) {
|
|||||||
|
|
||||||
apiTest := newHarborAPI()
|
apiTest := newHarborAPI()
|
||||||
|
|
||||||
endTimestamp := time.Now().Unix()
|
query := &apilib.LogQuery{
|
||||||
startTimestamp := endTimestamp - 3600
|
|
||||||
accessLog := &apilib.AccessLogFilter{
|
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Keywords: "",
|
Repository: "",
|
||||||
BeginTimestamp: startTimestamp,
|
Tag: "",
|
||||||
EndTimestamp: endTimestamp,
|
Operation: []string{""},
|
||||||
|
BeginTimestamp: 0,
|
||||||
|
EndTimestamp: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
//-------------------case1: Response Code=200------------------------------//
|
//-------------------case1: Response Code=200------------------------------//
|
||||||
fmt.Println("case 1: respose code:200")
|
fmt.Println("case 1: respose code:200")
|
||||||
projectID := "1"
|
projectID := "1"
|
||||||
httpStatusCode, _, err := apiTest.ProjectLogsFilter(*admin, projectID, *accessLog)
|
httpStatusCode, _, err := apiTest.ProjectLogs(*admin, projectID, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("Error while search access logs")
|
t.Error("Error while search access logs")
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
@ -356,7 +356,7 @@ func TestProjectLogsFilter(t *testing.T) {
|
|||||||
//-------------------case2: Response Code=401:User need to log in first.------------------------------//
|
//-------------------case2: Response Code=401:User need to log in first.------------------------------//
|
||||||
fmt.Println("case 2: respose code:401:User need to log in first.")
|
fmt.Println("case 2: respose code:401:User need to log in first.")
|
||||||
projectID = "1"
|
projectID = "1"
|
||||||
httpStatusCode, _, err = apiTest.ProjectLogsFilter(*unknownUsr, projectID, *accessLog)
|
httpStatusCode, _, err = apiTest.ProjectLogs(*unknownUsr, projectID, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("Error while search access logs")
|
t.Error("Error while search access logs")
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
@ -366,7 +366,7 @@ func TestProjectLogsFilter(t *testing.T) {
|
|||||||
//-------------------case3: Response Code=404:Project does not exist.-------------------------//
|
//-------------------case3: Response Code=404:Project does not exist.-------------------------//
|
||||||
fmt.Println("case 3: respose code:404:Illegal format of provided ID value.")
|
fmt.Println("case 3: respose code:404:Illegal format of provided ID value.")
|
||||||
projectID = "11111"
|
projectID = "11111"
|
||||||
httpStatusCode, _, err = apiTest.ProjectLogsFilter(*admin, projectID, *accessLog)
|
httpStatusCode, _, err = apiTest.ProjectLogs(*admin, projectID, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("Error while search access logs")
|
t.Error("Error while search access logs")
|
||||||
t.Log(err)
|
t.Log(err)
|
||||||
|
@ -60,6 +60,7 @@ func (ec envPolicyChecker) contentTrustEnabled(name string) bool {
|
|||||||
return os.Getenv("PROJECT_CONTENT_TRUST") == "1"
|
return os.Getenv("PROJECT_CONTENT_TRUST") == "1"
|
||||||
}
|
}
|
||||||
func (ec envPolicyChecker) vulnerableEnabled(name string) bool {
|
func (ec envPolicyChecker) vulnerableEnabled(name string) bool {
|
||||||
|
// TODO: May need get more information in vulnerable policies.
|
||||||
return os.Getenv("PROJECT_VULNERABBLE") == "1"
|
return os.Getenv("PROJECT_VULNERABBLE") == "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,8 +162,8 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
|
|||||||
log.Debugf("Passing the response to outter responseWriter")
|
log.Debugf("Passing the response to outter responseWriter")
|
||||||
copyResp(rec, rw)
|
copyResp(rec, rw)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("digest miamatch, failing the response.")
|
log.Debugf("digest mismatch, failing the response.")
|
||||||
http.Error(rw, "Failure in content trust handler", http.StatusPreconditionFailed)
|
http.Error(rw, "The image is not signed in Notary.", http.StatusPreconditionFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ func Init(urls ...string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
Proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
Proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
||||||
|
//TODO: add vulnerable interceptor.
|
||||||
handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: Proxy}}}
|
handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: Proxy}}}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ func initRouters() {
|
|||||||
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post;head:Head")
|
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]+)", &api.ProjectAPI{})
|
||||||
beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
|
beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
|
||||||
beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog")
|
beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs")
|
||||||
beego.Router("/api/statistics", &api.StatisticAPI{})
|
beego.Router("/api/statistics", &api.StatisticAPI{})
|
||||||
beego.Router("/api/users/?:id", &api.UserAPI{})
|
beego.Router("/api/users/?:id", &api.UserAPI{})
|
||||||
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
|
beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword")
|
||||||
|
@ -1,73 +1,74 @@
|
|||||||
{
|
{
|
||||||
"name": "harbor-ui",
|
"name": "harbor-ui",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
||||||
"lint": "tslint \"src/**/*.ts\"",
|
"lint": "tslint \"src/**/*.ts\"",
|
||||||
"test": "ng test --single-run",
|
"test": "ng test --single-run",
|
||||||
"test:once": "karma start karma.conf.js --single-run",
|
"test:once": "karma start karma.conf.js --single-run",
|
||||||
"pree2e": "webdriver-manager update",
|
"pree2e": "webdriver-manager update",
|
||||||
"e2e": "protractor",
|
"e2e": "protractor",
|
||||||
"cleanup": "rimraf dist",
|
"cleanup": "rimraf dist",
|
||||||
"copy": "copyfiles -f README.md LICENSE AUTHORS pkg/package.json dist",
|
"copy": "copyfiles -f README.md LICENSE AUTHORS pkg/package.json dist",
|
||||||
"transpile": "ngc -p tsconfig.json",
|
"transpile": "ngc -p tsconfig.json",
|
||||||
"package": "rollup -c",
|
"package": "rollup -c",
|
||||||
"minify": "uglifyjs dist/bundles/harborui.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/harborui.umd.min.js",
|
"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"
|
"build": "npm run cleanup && npm run transpile && npm run package && npm run minify && npm run copy"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^4.1.0",
|
"@angular/animations": "^4.1.0",
|
||||||
"@angular/common": "^4.1.0",
|
"@angular/common": "^4.1.0",
|
||||||
"@angular/compiler": "^4.1.0",
|
"@angular/compiler": "^4.1.0",
|
||||||
"@angular/core": "^4.1.0",
|
"@angular/core": "^4.1.0",
|
||||||
"@angular/forms": "^4.1.0",
|
"@angular/forms": "^4.1.0",
|
||||||
"@angular/http": "^4.1.0",
|
"@angular/http": "^4.1.0",
|
||||||
"@angular/platform-browser": "^4.1.0",
|
"@angular/platform-browser": "^4.1.0",
|
||||||
"@angular/platform-browser-dynamic": "^4.1.0",
|
"@angular/platform-browser-dynamic": "^4.1.0",
|
||||||
"@angular/router": "^4.1.0",
|
"@angular/router": "^4.1.0",
|
||||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
"@ngx-translate/core": "^6.0.0",
|
||||||
"web-animations-js": "^2.2.1",
|
"@ngx-translate/http-loader": "0.0.3",
|
||||||
"clarity-angular": "^0.9.7",
|
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||||
"clarity-icons": "^0.9.7",
|
"clarity-angular": "^0.9.7",
|
||||||
"clarity-ui": "^0.9.7",
|
"clarity-icons": "^0.9.7",
|
||||||
"core-js": "^2.4.1",
|
"clarity-ui": "^0.9.7",
|
||||||
"rxjs": "^5.0.1",
|
"core-js": "^2.4.1",
|
||||||
"ts-helpers": "^1.1.1",
|
"intl": "^1.2.5",
|
||||||
"zone.js": "^0.8.4",
|
"mutationobserver-shim": "^0.3.2",
|
||||||
"mutationobserver-shim": "^0.3.2",
|
"ngx-clipboard": "^8.0.2",
|
||||||
"@ngx-translate/core": "^6.0.0",
|
"ngx-cookie": "^1.0.0",
|
||||||
"@ngx-translate/http-loader": "0.0.3",
|
"rxjs": "^5.0.1",
|
||||||
"ngx-cookie": "^1.0.0",
|
"ts-helpers": "^1.1.1",
|
||||||
"intl": "^1.2.5"
|
"web-animations-js": "^2.2.1",
|
||||||
},
|
"zone.js": "^0.8.4"
|
||||||
"devDependencies": {
|
},
|
||||||
"@angular/cli": "^1.0.0",
|
"devDependencies": {
|
||||||
"@angular/compiler-cli": "^4.0.1",
|
"@angular/cli": "^1.0.0",
|
||||||
"@types/core-js": "^0.9.41",
|
"@angular/compiler-cli": "^4.0.1",
|
||||||
"@types/jasmine": "~2.2.30",
|
"@types/core-js": "^0.9.41",
|
||||||
"@types/node": "^6.0.42",
|
"@types/jasmine": "~2.2.30",
|
||||||
"bootstrap": "4.0.0-alpha.5",
|
"@types/node": "^6.0.42",
|
||||||
"codelyzer": "~2.0.0-beta.4",
|
"bootstrap": "4.0.0-alpha.5",
|
||||||
"enhanced-resolve": "^3.0.0",
|
"codelyzer": "~2.0.0-beta.4",
|
||||||
"jasmine-core": "2.4.1",
|
"enhanced-resolve": "^3.0.0",
|
||||||
"jasmine-spec-reporter": "2.5.0",
|
"jasmine-core": "2.4.1",
|
||||||
"karma": "1.2.0",
|
"jasmine-spec-reporter": "2.5.0",
|
||||||
"karma-cli": "^1.0.1",
|
"karma": "1.2.0",
|
||||||
"karma-jasmine": "^1.0.2",
|
"karma-cli": "^1.0.1",
|
||||||
"karma-mocha-reporter": "^2.2.1",
|
"karma-jasmine": "^1.0.2",
|
||||||
"karma-phantomjs-launcher": "^1.0.0",
|
"karma-mocha-reporter": "^2.2.1",
|
||||||
"karma-remap-istanbul": "^0.2.1",
|
"karma-phantomjs-launcher": "^1.0.0",
|
||||||
"protractor": "^4.0.9",
|
"karma-remap-istanbul": "^0.2.1",
|
||||||
"rollup": "^0.41.6",
|
"protractor": "^4.0.9",
|
||||||
"ts-node": "1.2.1",
|
"rollup": "^0.41.6",
|
||||||
"tslint": "^4.1.1",
|
"ts-node": "1.2.1",
|
||||||
"typescript": "~2.2.0",
|
"tslint": "^4.1.1",
|
||||||
"typings": "^1.4.0",
|
"typescript": "~2.2.0",
|
||||||
"uglify-js": "^2.8.22",
|
"typings": "^1.4.0",
|
||||||
"webdriver-manager": "10.2.5",
|
"uglify-js": "^2.8.22",
|
||||||
"rimraf": "^2.6.1",
|
"webdriver-manager": "10.2.5",
|
||||||
"copyfiles": "^1.2.0"
|
"rimraf": "^2.6.1",
|
||||||
}
|
"copyfiles": "^1.2.0"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ import { CONFIRMATION_DIALOG_DIRECTIVES } from './confirmation-dialog/index';
|
|||||||
import { INLINE_ALERT_DIRECTIVES } from './inline-alert/index';
|
import { INLINE_ALERT_DIRECTIVES } from './inline-alert/index';
|
||||||
import { DATETIME_PICKER_DIRECTIVES } from './datetime-picker/index';
|
import { DATETIME_PICKER_DIRECTIVES } from './datetime-picker/index';
|
||||||
import { VULNERABILITY_DIRECTIVES } from './vulnerability-scanning/index';
|
import { VULNERABILITY_DIRECTIVES } from './vulnerability-scanning/index';
|
||||||
|
import { PUSH_IMAGE_BUTTON_DIRECTIVES } from './push-image/index';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SystemInfoService,
|
SystemInfoService,
|
||||||
@ -142,7 +143,8 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
|||||||
LIST_REPLICATION_RULE_DIRECTIVES,
|
LIST_REPLICATION_RULE_DIRECTIVES,
|
||||||
CREATE_EDIT_RULE_DIRECTIVES,
|
CREATE_EDIT_RULE_DIRECTIVES,
|
||||||
DATETIME_PICKER_DIRECTIVES,
|
DATETIME_PICKER_DIRECTIVES,
|
||||||
VULNERABILITY_DIRECTIVES
|
VULNERABILITY_DIRECTIVES,
|
||||||
|
PUSH_IMAGE_BUTTON_DIRECTIVES
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
LOG_DIRECTIVES,
|
LOG_DIRECTIVES,
|
||||||
@ -160,6 +162,7 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
|||||||
CREATE_EDIT_RULE_DIRECTIVES,
|
CREATE_EDIT_RULE_DIRECTIVES,
|
||||||
DATETIME_PICKER_DIRECTIVES,
|
DATETIME_PICKER_DIRECTIVES,
|
||||||
VULNERABILITY_DIRECTIVES,
|
VULNERABILITY_DIRECTIVES,
|
||||||
|
PUSH_IMAGE_BUTTON_DIRECTIVES,
|
||||||
TranslateModule
|
TranslateModule
|
||||||
],
|
],
|
||||||
providers: []
|
providers: []
|
||||||
|
@ -12,4 +12,5 @@ export * from './tag/index';
|
|||||||
export * from './list-replication-rule/index';
|
export * from './list-replication-rule/index';
|
||||||
export * from './replication/index';
|
export * from './replication/index';
|
||||||
export * from './vulnerability-scanning/index';
|
export * from './vulnerability-scanning/index';
|
||||||
export * from './i18n/index';
|
export * from './i18n/index';
|
||||||
|
export * from './push-image/index';
|
48
src/ui_ng/lib/src/push-image/copy-input.component.ts
Normal file
48
src/ui_ng/lib/src/push-image/copy-input.component.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
|
||||||
|
import { COPY_INPUT_HTML } from './copy-input.html';
|
||||||
|
import { PUSH_IMAGE_STYLE } from './push-image.css';
|
||||||
|
|
||||||
|
export const enum CopyStatus {
|
||||||
|
NORMAL, SUCCESS, ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'hbr-copy-input',
|
||||||
|
styles: [PUSH_IMAGE_STYLE],
|
||||||
|
template: COPY_INPUT_HTML,
|
||||||
|
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class CopyInputComponent {
|
||||||
|
@Input() inputSize: number = 40;
|
||||||
|
@Input() headerTitle: string = "Copy Input";
|
||||||
|
@Input() defaultValue: string = "N/A";
|
||||||
|
|
||||||
|
state: CopyStatus = CopyStatus.NORMAL;
|
||||||
|
|
||||||
|
@Output() onCopySuccess: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
@Output() onCopyError: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
|
onSuccess($event: any): void {
|
||||||
|
this.state = CopyStatus.SUCCESS;
|
||||||
|
this.onCopySuccess.emit($event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onError(error: any): void {
|
||||||
|
this.state = CopyStatus.ERROR;
|
||||||
|
this.onCopyError.emit(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.state = CopyStatus.NORMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isCopied(): boolean {
|
||||||
|
return this.state === CopyStatus.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasCopyError(): boolean {
|
||||||
|
return this.state === CopyStatus.ERROR;
|
||||||
|
}
|
||||||
|
}
|
15
src/ui_ng/lib/src/push-image/copy-input.html.ts
Normal file
15
src/ui_ng/lib/src/push-image/copy-input.html.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const COPY_INPUT_HTML: string = `
|
||||||
|
<div>
|
||||||
|
<div class="command-title">
|
||||||
|
{{headerTitle}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
<input type="text" class="command-input" size="{{inputSize}}" [(ngModel)]="defaultValue" #inputTarget readonly/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<clr-icon shape="copy" [class.is-success]="isCopied" [class.is-error]="hasCopyError" class="info-tips-icon" size="24" [ngxClipboard]="inputTarget" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)"></clr-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
11
src/ui_ng/lib/src/push-image/index.ts
Normal file
11
src/ui_ng/lib/src/push-image/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Type } from "@angular/core";
|
||||||
|
import { PushImageButtonComponent } from './push-image.component';
|
||||||
|
import { CopyInputComponent } from './copy-input.component';
|
||||||
|
|
||||||
|
export * from "./push-image.component";
|
||||||
|
export * from './copy-input.component';
|
||||||
|
|
||||||
|
export const PUSH_IMAGE_BUTTON_DIRECTIVES: Type<any>[] = [
|
||||||
|
CopyInputComponent,
|
||||||
|
PushImageButtonComponent
|
||||||
|
];
|
64
src/ui_ng/lib/src/push-image/push-image.component.spec.ts
Normal file
64
src/ui_ng/lib/src/push-image/push-image.component.spec.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { HttpModule } from '@angular/http';
|
||||||
|
import { DebugElement } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
import { PushImageButtonComponent } from './push-image.component';
|
||||||
|
import { CopyInputComponent } from './copy-input.component';
|
||||||
|
import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
|
||||||
|
|
||||||
|
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { click } from '../utils';
|
||||||
|
|
||||||
|
describe('PushImageButtonComponent (inline template)', () => {
|
||||||
|
let component: PushImageButtonComponent;
|
||||||
|
let fixture: ComponentFixture<PushImageButtonComponent>;
|
||||||
|
let serviceConfig: IServiceConfig;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
SharedModule
|
||||||
|
],
|
||||||
|
declarations: [InlineAlertComponent, CopyInputComponent, PushImageButtonComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: SERVICE_CONFIG, useValue: {} }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(PushImageButtonComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
component.projectName = "testing";
|
||||||
|
component.registryUrl = "https://testing.harbor.com"
|
||||||
|
serviceConfig = TestBed.get(SERVICE_CONFIG);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the drop-down panel', async(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
let el: HTMLElement = fixture.nativeElement.querySelector('button');
|
||||||
|
expect(el).not.toBeNull();
|
||||||
|
el.click();
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
let copyInputs: HTMLInputElement[] = fixture.nativeElement.querySelectorAll('.command-input');
|
||||||
|
expect(copyInputs.length).toEqual(2);
|
||||||
|
|
||||||
|
expect(copyInputs[0].value.trim()).toEqual(`docker tag SOURCE_IMAGE[:TAG] ${component.registryUrl}/${component.projectName}/IMAGE[:TAG]`);
|
||||||
|
expect(copyInputs[1].value.trim()).toEqual(`docker push ${component.registryUrl}/${component.projectName}/IMAGE[:TAG]`);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
51
src/ui_ng/lib/src/push-image/push-image.component.ts
Normal file
51
src/ui_ng/lib/src/push-image/push-image.component.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Component, Input, ViewChild } from '@angular/core';
|
||||||
|
import { CopyInputComponent } from './copy-input.component';
|
||||||
|
import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
|
||||||
|
|
||||||
|
import { PUSH_IMAGE_STYLE } from './push-image.css';
|
||||||
|
import { PUSH_IMAGE_HTML } from './push-image.html';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'hbr-push-image-button',
|
||||||
|
template: PUSH_IMAGE_HTML,
|
||||||
|
styles: [PUSH_IMAGE_STYLE],
|
||||||
|
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class PushImageButtonComponent {
|
||||||
|
@Input() registryUrl: string = "unknown";
|
||||||
|
@Input() projectName: string = "unknown";
|
||||||
|
|
||||||
|
@ViewChild("tagCopy") tagCopyInput: CopyInputComponent;
|
||||||
|
@ViewChild("pushCopy") pushCopyInput: CopyInputComponent;
|
||||||
|
@ViewChild("copyAlert") copyAlert: InlineAlertComponent;
|
||||||
|
|
||||||
|
|
||||||
|
public get tagCommand(): string {
|
||||||
|
return `docker tag SOURCE_IMAGE[:TAG] ${this.registryUrl}/${this.projectName}/IMAGE[:TAG]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get pushCommand(): string {
|
||||||
|
return `docker push ${this.registryUrl}/${this.projectName}/IMAGE[:TAG]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onclick(): void {
|
||||||
|
if (this.tagCopyInput) {
|
||||||
|
this.tagCopyInput.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pushCopyInput) {
|
||||||
|
this.pushCopyInput.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.copyAlert){
|
||||||
|
this.copyAlert.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCpError($event: any): void {
|
||||||
|
if(this.copyAlert){
|
||||||
|
this.copyAlert.showInlineError("PUSH_IMAGE.COPY_ERROR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/ui_ng/lib/src/push-image/push-image.css.ts
Normal file
42
src/ui_ng/lib/src/push-image/push-image.css.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
export const PUSH_IMAGE_STYLE: string = `
|
||||||
|
.commands-container {
|
||||||
|
min-width: 360px;
|
||||||
|
max-width: 720px;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-override {
|
||||||
|
display: inline-block !important;
|
||||||
|
margin-top: 0px !important;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.commands-section {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-tips-icon {
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-tips-icon:hover {
|
||||||
|
color: #007CBB;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-title {
|
||||||
|
font-size: 14px;
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-input {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host>>>.dropdown-menu {
|
||||||
|
min-width: 360px;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
`;
|
34
src/ui_ng/lib/src/push-image/push-image.html.ts
Normal file
34
src/ui_ng/lib/src/push-image/push-image.html.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export const PUSH_IMAGE_HTML: string = `
|
||||||
|
<div>
|
||||||
|
<clr-dropdown [clrMenuPosition]="'bottom-right'">
|
||||||
|
<button class="btn btn-link" clrDropdownToggle (click)="onclick()">
|
||||||
|
{{ 'PUSH_IMAGE.TITLE' | translate | uppercase}}
|
||||||
|
<clr-icon shape="caret down"></clr-icon>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu" style="min-width:500px;">
|
||||||
|
<div class="commands-container">
|
||||||
|
<section>
|
||||||
|
<span><h5 class="h5-override">{{ 'PUSH_IMAGE.TITLE' | translate }}</h5></span>
|
||||||
|
<span>
|
||||||
|
<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">{{ 'PUSH_IMAGE.TOOLTIP' | translate }}</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<inline-alert #copyAlert></inline-alert>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<article class="commands-section">
|
||||||
|
<hbr-copy-input #tagCopy (onCopyError)="onCpError($event)" inputSize="50" headerTitle="{{ 'PUSH_IMAGE.TAG_COMMAND' | translate }}" defaultValue="{{tagCommand}}"></hbr-copy-input>
|
||||||
|
</article>
|
||||||
|
<article class="commands-section">
|
||||||
|
<hbr-copy-input #pushCopy (onCopyError)="onCpError($event)" inputSize="50" headerTitle="{{ 'PUSH_IMAGE.PUSH_COMMAND' | translate }}" defaultValue="{{pushCommand}}"></hbr-copy-input>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-dropdown>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -3,7 +3,7 @@ export const REPLICATION_TEMPLATE: string = `
|
|||||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||||
<div class="row flex-items-xs-between">
|
<div class="row flex-items-xs-between">
|
||||||
<div class="flex-xs-middle option-left">
|
<div class="flex-xs-middle option-left">
|
||||||
<button *ngIf="withReplicationJob" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
|
<button *ngIf="projectId" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
|
||||||
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
|
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-xs-middle option-right">
|
<div class="flex-xs-middle option-right">
|
||||||
|
@ -114,9 +114,6 @@ export class ReplicationComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if(!this.projectId) {
|
|
||||||
this.errorHandler.warning('Project ID is unset.');
|
|
||||||
}
|
|
||||||
this.currentRuleStatus = this.ruleStatus[0];
|
this.currentRuleStatus = this.ruleStatus[0];
|
||||||
this.currentJobStatus = this.jobStatus[0];
|
this.currentJobStatus = this.jobStatus[0];
|
||||||
this.currentJobSearchOption = 0;
|
this.currentJobSearchOption = 0;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
export const REPOSITORY_STACKVIEW_STYLES: string = `
|
export const REPOSITORY_STACKVIEW_STYLES: string = `
|
||||||
.option-right {
|
.option-right {
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
margin-top: 32px;
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
export const REPOSITORY_STYLE = `.option-right {
|
export const REPOSITORY_STYLE = `.option-right {
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
margin-top: 32px;
|
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}`;
|
}`;
|
@ -9,6 +9,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
|||||||
import { TranslatorJsonLoader } from '../i18n/local-json.loader';
|
import { TranslatorJsonLoader } from '../i18n/local-json.loader';
|
||||||
import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
|
import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
|
||||||
import { CookieService, CookieModule } from 'ngx-cookie';
|
import { CookieService, CookieModule } from 'ngx-cookie';
|
||||||
|
import { ClipboardModule } from 'ngx-clipboard';
|
||||||
|
|
||||||
/*export function HttpLoaderFactory(http: Http) {
|
/*export function HttpLoaderFactory(http: Http) {
|
||||||
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
|
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
|
||||||
@ -40,6 +41,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
HttpModule,
|
HttpModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
ClipboardModule,
|
||||||
CookieModule.forRoot(),
|
CookieModule.forRoot(),
|
||||||
ClarityModule.forRoot(),
|
ClarityModule.forRoot(),
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
@ -59,6 +61,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
|
|||||||
HttpModule,
|
HttpModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
CookieModule,
|
CookieModule,
|
||||||
|
ClipboardModule,
|
||||||
ClarityModule,
|
ClarityModule,
|
||||||
TranslateModule
|
TranslateModule
|
||||||
],
|
],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="global-message-alert" [hidden]="hideOuter">
|
<div [class.global-message-alert]="!isAppLevel">
|
||||||
<clr-alert [clrAlertType]="globalMessage.type" [clrAlertAppLevel]="isAppLevel" [(clrAlertClosed)]="!globalMessageOpened" (clrAlertClosedChange)="onClose()">
|
<clr-alert [clrAlertType]="globalMessage.type" [clrAlertAppLevel]="isAppLevel" [(clrAlertClosed)]="!globalMessageOpened" (clrAlertClosedChange)="onClose()">
|
||||||
<div class="alert-item">
|
<div class="alert-item">
|
||||||
<span class="alert-text">
|
<span class="alert-text">
|
||||||
|
@ -33,8 +33,7 @@ export class MessageComponent implements OnInit, OnDestroy {
|
|||||||
globalMessageOpened: boolean;
|
globalMessageOpened: boolean;
|
||||||
messageText: string = "";
|
messageText: string = "";
|
||||||
timer: any = null;
|
timer: any = null;
|
||||||
hideOuter: boolean = true;
|
|
||||||
|
|
||||||
appLevelMsgSub: Subscription;
|
appLevelMsgSub: Subscription;
|
||||||
msgSub: Subscription;
|
msgSub: Subscription;
|
||||||
clearSub: Subscription;
|
clearSub: Subscription;
|
||||||
@ -79,8 +78,6 @@ export class MessageComponent implements OnInit, OnDestroy {
|
|||||||
let hackDom: any = queryDoms[0];
|
let hackDom: any = queryDoms[0];
|
||||||
hackDom.className += ' alert-global alert-global-align';
|
hackDom.className += ' alert-global alert-global-align';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hideOuter = false;
|
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -138,6 +135,5 @@ export class MessageComponent implements OnInit, OnDestroy {
|
|||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
}
|
}
|
||||||
this.globalMessageOpened = false;
|
this.globalMessageOpened = false;
|
||||||
this.hideOuter = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -151,7 +151,7 @@ export class AuditLogComponent implements OnInit {
|
|||||||
for(var i in this.filterOptions) {
|
for(var i in this.filterOptions) {
|
||||||
let filterOption = this.filterOptions[i];
|
let filterOption = this.filterOptions[i];
|
||||||
if(filterOption.checked) {
|
if(filterOption.checked) {
|
||||||
operationFilter.push(this.filterOptions[i].key);
|
operationFilter.push('operation=' + this.filterOptions[i].key);
|
||||||
}else{
|
}else{
|
||||||
selectAll = false;
|
selectAll = false;
|
||||||
}
|
}
|
||||||
@ -159,7 +159,7 @@ export class AuditLogComponent implements OnInit {
|
|||||||
if(selectAll) {
|
if(selectAll) {
|
||||||
operationFilter = [];
|
operationFilter = [];
|
||||||
}
|
}
|
||||||
this.queryParam.keywords = operationFilter.join('/');
|
this.queryParam.keywords = operationFilter.join('&');
|
||||||
this.retrieve();
|
this.retrieve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Http, Headers, RequestOptions } from '@angular/http';
|
import { Http, Headers, RequestOptions, URLSearchParams } from '@angular/http';
|
||||||
|
|
||||||
import { AuditLog } from './audit-log';
|
import { AuditLog } from './audit-log';
|
||||||
|
|
||||||
@ -35,15 +35,24 @@ export class AuditLogService {
|
|||||||
constructor(private http: Http) {}
|
constructor(private http: Http) {}
|
||||||
|
|
||||||
listAuditLogs(queryParam: AuditLog): Observable<any> {
|
listAuditLogs(queryParam: AuditLog): Observable<any> {
|
||||||
|
let params: URLSearchParams = new URLSearchParams(queryParam.keywords);
|
||||||
|
if(queryParam.begin_timestamp) {
|
||||||
|
params.set('begin_timestamp', <string>queryParam.begin_timestamp);
|
||||||
|
}
|
||||||
|
if(queryParam.end_timestamp) {
|
||||||
|
params.set('end_timestamp', <string>queryParam.end_timestamp);
|
||||||
|
}
|
||||||
|
if(queryParam.username) {
|
||||||
|
params.set('username', queryParam.username);
|
||||||
|
}
|
||||||
|
if(queryParam.page) {
|
||||||
|
params.set('page', <string>queryParam.page);
|
||||||
|
}
|
||||||
|
if(queryParam.page_size) {
|
||||||
|
params.set('page_size', <string>queryParam.page_size);
|
||||||
|
}
|
||||||
return this.http
|
return this.http
|
||||||
.post(`/api/projects/${queryParam.project_id}/logs/filter?page=${queryParam.page}&page_size=${queryParam.page_size}`, {
|
.get(`/api/projects/${queryParam.project_id}/logs`, {params: params})
|
||||||
begin_timestamp: queryParam.begin_timestamp,
|
|
||||||
end_timestamp: queryParam.end_timestamp,
|
|
||||||
keywords: queryParam.keywords,
|
|
||||||
operation: queryParam.operation,
|
|
||||||
project_id: queryParam.project_id,
|
|
||||||
username: queryParam.username
|
|
||||||
})
|
|
||||||
.map(response => response)
|
.map(response => response)
|
||||||
.catch(error => Observable.throw(error));
|
.catch(error => Observable.throw(error));
|
||||||
}
|
}
|
||||||
|
@ -30,18 +30,18 @@
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
export class AuditLog {
|
export class AuditLog {
|
||||||
log_id: number;
|
log_id: number | string;
|
||||||
project_id: number;
|
project_id: number | string;
|
||||||
username: string;
|
username: string;
|
||||||
repo_name: string;
|
repo_name: string;
|
||||||
repo_tag: string;
|
repo_tag: string;
|
||||||
operation: string;
|
operation: string;
|
||||||
op_time: Date;
|
op_time: Date;
|
||||||
begin_timestamp: number = 0;
|
begin_timestamp: number | string;
|
||||||
end_timestamp: number = 0;
|
end_timestamp: number | string;
|
||||||
keywords: string;
|
keywords: string;
|
||||||
page: number;
|
page: number | string;
|
||||||
page_size: number;
|
page_size: number | string;
|
||||||
fromTime: string;
|
fromTime: string;
|
||||||
toTime: string;
|
toTime: string;
|
||||||
}
|
}
|
@ -12,7 +12,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<clr-datagrid>
|
<clr-datagrid [clrDgLoading]="inProgress">
|
||||||
<clr-dg-column>{{'USER.COLUMN_NAME' | translate}}</clr-dg-column>
|
<clr-dg-column>{{'USER.COLUMN_NAME' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column>{{'USER.COLUMN_ADMIN' | translate}}</clr-dg-column>
|
<clr-dg-column>{{'USER.COLUMN_ADMIN' | translate}}</clr-dg-column>
|
||||||
<clr-dg-column>{{'USER.COLUMN_EMAIL' | translate}}</clr-dg-column>
|
<clr-dg-column>{{'USER.COLUMN_EMAIL' | translate}}</clr-dg-column>
|
||||||
@ -27,7 +27,10 @@
|
|||||||
<clr-dg-cell>{{user.email}}</clr-dg-cell>
|
<clr-dg-cell>{{user.email}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{user.creation_time | date: 'short'}}</clr-dg-cell>
|
<clr-dg-cell>{{user.creation_time | date: 'short'}}</clr-dg-cell>
|
||||||
</clr-dg-row>
|
</clr-dg-row>
|
||||||
<clr-dg-footer>{{users.length}} {{'USER.ITEMS' | translate}}</clr-dg-footer>
|
<clr-dg-footer>{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} of {{pagination.totalItems}} users
|
||||||
|
<clr-dg-pagination #pagination [clrDgPageSize]="15" [clrDgTotalItems]="users.length"> {{'USER.ITEMS' | translate}}
|
||||||
|
</clr-dg-pagination>
|
||||||
|
</clr-dg-footer>
|
||||||
</clr-datagrid>
|
</clr-datagrid>
|
||||||
</div>
|
</div>
|
||||||
<new-user-modal (addNew)="addUserToList($event)"></new-user-modal>
|
<new-user-modal (addNew)="addUserToList($event)"></new-user-modal>
|
||||||
|
@ -38,10 +38,10 @@ import { AppConfigService } from '../app-config.service';
|
|||||||
export class UserComponent implements OnInit, OnDestroy {
|
export class UserComponent implements OnInit, OnDestroy {
|
||||||
users: User[] = [];
|
users: User[] = [];
|
||||||
originalUsers: Promise<User[]>;
|
originalUsers: Promise<User[]>;
|
||||||
onGoing: boolean = false;
|
private onGoing: boolean = true;
|
||||||
adminMenuText: string = "";
|
private adminMenuText: string = "";
|
||||||
adminColumn: string = "";
|
private adminColumn: string = "";
|
||||||
deletionSubscription: Subscription;
|
private deletionSubscription: Subscription;
|
||||||
|
|
||||||
currentTerm: string;
|
currentTerm: string;
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* Harbor API
|
* Harbor API
|
||||||
*
|
*
|
||||||
* These APIs provide services for manipulating Harbor project.
|
* These APIs provide services for manipulating Harbor project.
|
||||||
*
|
*
|
||||||
* OpenAPI spec version: 0.3.0
|
* OpenAPI spec version: 0.3.0
|
||||||
*
|
*
|
||||||
* Generated by: https://github.com/swagger-api/swagger-codegen.git
|
* Generated by: https://github.com/swagger-api/swagger-codegen.git
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@ -22,17 +22,13 @@
|
|||||||
|
|
||||||
package apilib
|
package apilib
|
||||||
|
|
||||||
type AccessLogFilter struct {
|
type LogQuery struct {
|
||||||
|
Username string `json:"username"`
|
||||||
// Relevant user's name that accessed this project.
|
Repository string `json:"repository"`
|
||||||
Username string `json:"username,omitempty"`
|
Tag string `json:"tag"`
|
||||||
|
Operation []string `json:"operation"`
|
||||||
// Operation name specified when project created.
|
BeginTimestamp int64 `json:"begin_timestamp"`
|
||||||
Keywords string `json:"keywords,omitempty"`
|
EndTimestamp int64 `json:"end_timestamp"`
|
||||||
|
Page int64 `json:"page"`
|
||||||
// Begin timestamp for querying access logs.
|
PageSize int64 `json:"page_size"`
|
||||||
BeginTimestamp int64 `json:"begin_timestamp,omitempty"`
|
|
||||||
|
|
||||||
// End timestamp for querying accessl logs.
|
|
||||||
EndTimestamp int64 `json:"end_timestamp,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user