Merge branch 'master' into https-install

This commit is contained in:
Stuart Clements 2019-10-23 12:08:19 +02:00 committed by GitHub
commit b77fd20865
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 867 additions and 312 deletions

View File

@ -1,23 +1,23 @@
sudo: true
language: go
go:
- 1.12.5
- 1.12.12
go_import_path: github.com/goharbor/harbor
services:
- docker
dist: trusty
matrix:
include:
- go: 1.12.5
- go: 1.12.12
env:
- UTTEST=true
- go: 1.12.5
- go: 1.12.12
env:
- APITEST_DB=true
- go: 1.12.5
- go: 1.12.12
env:
- APITEST_LDAP=true
- go: 1.12.5
- go: 1.12.12
env:
- OFFLINE=true
- language: node_js

View File

@ -80,3 +80,6 @@ feature within Harbor before deploying images into production.
**Allegis:** Harbor is used at Allegis as a secure private registry to store
and scan customized container images for different business applications, like
ELK stack, as part of their CI/CD pipeline.
# Adding a logo
If you would like to add your logo to the `Users and Partners of Harbor` section of the website, add a PNG version of your logo to the docs/img directory in this repo and submit a pull request with your change. Name the image file something that reflects your company (e.g., if your company is called Acme, name the image acme.png). We will follow up and make the change in the goharbor.io website as well.

View File

@ -127,6 +127,7 @@ Harbor backend is written in [Go](http://golang.org/). If you don't have a Harbo
| 1.6 | 1.9.2 |
| 1.7 | 1.9.2 |
| 1.8 | 1.11.2 |
| 1.9 | 1.12.12 |
Ensure your GOPATH and PATH have been configured in accordance with the Go environment instructions.

View File

@ -9,7 +9,7 @@
# compile_golangimage:
# compile from golang image
# for example: make compile_golangimage -e GOBUILDIMAGE= \
# golang:1.11.2
# golang:1.12.12
# compile_core, compile_jobservice: compile specific binary
#
# build: build Harbor docker images from photon baseimage
@ -111,6 +111,9 @@ CLAIRADAPTERVERSION=c7db8b15
# version of chartmuseum
CHARTMUSEUMVERSION=v0.9.0
# version of registry for pulling the source code
REGISTRY_SRC_TAG=v2.7.1
define VERSIONS_FOR_PREPARE
VERSION_TAG: $(VERSIONTAG)
REGISTRY_VERSION: $(REGISTRYVERSION)
@ -138,7 +141,7 @@ GOINSTALL=$(GOCMD) install
GOTEST=$(GOCMD) test
GODEP=$(GOTEST) -i
GOFMT=gofmt -w
GOBUILDIMAGE=golang:1.12.5
GOBUILDIMAGE=golang:1.12.12
GOBUILDPATH=/harbor
GOIMAGEBUILDCMD=/usr/local/go/bin/go
GOIMAGEBUILD=$(GOIMAGEBUILDCMD) build -mod vendor

View File

@ -3,28 +3,6 @@ guidelines.
[GOVERNANCE.md](https://github.com/goharbor/community/blob/master/GOVERNANCE.md)
describes governance guidelines and maintainer responsibilities.
Maintainer list here is a copy of [MAINTAINERS.md](https://github.com/goharbor/community/blob/master/MAINTAINERS.md).
[MAINTAINERS.md](https://github.com/goharbor/community/blob/master/MAINTAINERS.md) contains the project maintainers.
## Core Maintainers
| Core Maintainer | GitHub ID | Affiliation |
| --------------- | --------- | ----------- |
| Daniel Jiang | [reasonerjt](https://github.com/reasonerjt) | [VMware](https://www.github.com/vmware/) |
| Steven Ren | [renmaosheng](https://github.com/renmaosheng) | [VMware](https://www.github.com/vmware/) |
| Steven Zou | [steven-zou](https://github.com/steven-zou) | [VMware](https://www.github.com/vmware/) |
| Henry Zhang| [hainingzhang](https://github.com/hainingzhang)| [VMware](https://www.github.com/vmware/) |
| Michael Michael |[michmike](https://github.com/michmike)| [VMware](https://www.github.com/vmware/) |
## Maintainers
| Maintainer | GitHub ID | Affiliation |
| ---------- | --------- | ----------- |
| Daojun Zhang | [stonezdj](https://github.com/stonezdj) | [VMware](https://www.github.com/vmware/) |
| Wenkai Yin | [ywk253100](https://github.com/ywk253100) | [VMware](https://www.github.com/vmware/) |
| Yan Wang | [wy65701436](https://github.com/wy65701436) | [VMware](https://www.github.com/vmware/) |
| Qian Deng | [ninjadq](https://github.com/ninjadq) | [VMware](https://www.github.com/vmware/) |
| Mia Zhou | [zhoumeina](https://github.com/zhoumeina) | [VMware](https://www.github.com/vmware/) |
| Nathan Lowe | [nlowe](https://github.com/nlowe) | [Hyland Software](https://github.com/HylandSoftware) |
| De Chen | [cd1989](https://github.com/cd1989) | [Caicloud](https://github.com/caicloud) |
| Mingming Pei | [mmpei](https://github.com/mmpei) | [Netease](https://github.com/netease) |
| Fanjian Kong | [kofj](https://github.com/kofj) | [Qihoo360](https://github.com/Qihoo360) |
[GUIDING_PRINCIPLES.md](https://github.com/goharbor/community/blob/master/GUIDING_PRINCIPLES.md) contains the project vision, values and principles and how we apply them in making decisions.

View File

@ -11,17 +11,16 @@
|![notification](docs/img/bell-outline-badged.svg)Community Meeting|
|------------------|
|The Harbor Project holds bi-weekly community calls, to join them and watch previous meeting notes and recordings, please see [meeting schedule](https://github.com/goharbor/community/blob/master/MEETING_SCHEDULE.md).|
|The Harbor Project holds bi-weekly community calls in two different timezones. To join the community calls or to watch previous meeting notes and recordings, please visit the [meeting schedule](https://github.com/goharbor/community/blob/master/MEETING_SCHEDULE.md).|
Welcome to join below Harbor community events and meet with project maintainers and users:
We welcome you to join the below Harbor community events and meet with project maintainers and users:
**May 20-24, 2019**, [KubeCon EU, Barcelona](https://events.linuxfoundation.org/events/kubecon-cloudnativecon-europe-2019/): Harbor Community Reception, Intro and Deep-dive sessions.
**November 18-21, 2019**, [KubeCon US, San Diego](https://events19.linuxfoundation.org/events/kubecon-cloudnativecon-north-america-2019): Harbor Lunch & Learn led by Joe Beda, Intro and Deep-dive sessions.
**June 24-26, 2019**, [KubeCon Shanghai](https://www.lfasiallc.com/events/kubecon-cloudnativecon-china-2019/): Harbor community meetup, Harbor session.
</br> </br>
**Note**: The `master` branch may be in an *unstable or even broken state* during development.
Please use [releases](https://github.com/vmware/harbor/releases) instead of the `master` branch in order to get stable binaries.
Please use [releases](https://github.com/vmware/harbor/releases) instead of the `master` branch in order to get a stable set of binaries.
<img alt="Harbor" src="docs/img/harbor_logo.png">

View File

@ -2,40 +2,11 @@
### About this document
This document provides description of items that are gathered from the community and planned in Harbor's roadmap. This should serve as a reference point for Harbor users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan.
This document provides a link to the [Harbor Project board](https://github.com/orgs/goharbor/projects/1) that serves as the up to date description of items that are in the Harbor release pipeline. The board has separate swim lanes for each release. Most items are gathered from the community or include a feedback loop with the community. This should serve as a reference point for Harbor users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan.
### How to help?
Discussion on the roadmap can take place in threads under [Issues](https://github.com/vmware/harbor/issues). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort.
Discussion on the roadmap can take place in threads under [Issues](https://github.com/goharbor/harbor/issues) or in [community meetings](https://github.com/goharbor/community/blob/master/MEETING_SCHEDULE.md). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort.
### How to add an item to the roadmap?
Please open an issue to track any initiative on the roadmap of Harbor. We will work with and rely on our community to focus our efforts to improve Harbor.
---
### 1. Notary
The notary feature allows publishers to sign their images offline and to push the signed content to a notary server. This ensures the authenticity of images.
### 2. Vulnerability Scanning
The capability to scan images for vulnerability.
### 3. Image replication enhancement
To provide more sophisticated rule for image replication.
- Image filtering by tags
- Replication can be scheduled at a certain time using a rule like: one time only, daily, weekly, etc.
- Image deletion can have the option not to be replicated to a remote instance.
- Global replication rule: Instead of setting the rule of individual project, system admin can set a global rule for all projects.
- Project admin can set replication policy of the project.
### 4. Authentication (OAuth2)
In addition to LDAP/AD and local users, OAuth 2.0 can be used to authenticate a user.
### 5. High Availability
Support multi-node deployment of Harbor for high availability, scalability and load-balancing purposes.
### 6. Statistics and description for repositories
User can add a description to a repository. The access count of a repo can be aggregated and displayed.
### 7. Migration tool to move from an existing registry to Harbor
A tool to migrate images from a vanilla registry server to Harbor, without the need to export/import a large amount of data.
Please open an issue to track any initiative on the roadmap of Harbor (Usually driven by new feature requests). We will work with and rely on our community to focus our efforts to improve Harbor.

View File

@ -43,25 +43,25 @@ You can compile the code by one of the three approaches:
- Get official Golang image from docker hub:
```sh
$ docker pull golang:1.12.5
$ docker pull golang:1.12.12
```
- Build, install and bring up Harbor without Notary:
```sh
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage
$ make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage
```
- Build, install and bring up Harbor with Notary:
```sh
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
$ make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true
```
- Build, install and bring up Harbor with Clair:
```sh
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage CLAIRFLAG=true
$ make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage CLAIRFLAG=true
```
#### II. Compile code with your own Golang environment, then build Harbor

View File

@ -13,7 +13,7 @@ You can use certificates that are signed by a trusted third-party CA, or you ca
```
```
openssl req -x509 -new -nodes -sha512 -days 3650 \
-subj "/C=TW/ST=Taipei/L=Taipei/O=example/OU=Personal/CN=yourdomain.com" \
-subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=yourdomain.com" \
-key ca.key \
-out ca.crt
```
@ -36,7 +36,7 @@ If you use FQDN like **yourdomain.com** to connect your registry host, then you
```
openssl req -sha512 -new \
-subj "/C=TW/ST=Taipei/L=Taipei/O=example/OU=Personal/CN=yourdomain.com" \
-subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=yourdomain.com" \
-key yourdomain.com.key \
-out yourdomain.com.csr
```

View File

@ -1,18 +1,32 @@
# Registry Landscape
The cloud native ecosystem is moving rapidlyregistries and their featuresets are no exception. We've made our best effort to survey the container registry landscape and compare to our core featureset.
The cloud native ecosystem is moving rapidlyregistries and their feature sets are no exception. We've made our best effort to survey the container registry landscape and compare to our core feature set.
If you find something outdated or outright erroneous, please submit a PR and we'll fix it right away.
| Feature | Harbor | Docker Trusted Registry | Quay | Cloud Providers (GCP, AWS, Azure) | Docker Distribution | Artifactory |
| -------------: | :----: | :---------------------: | :--: | :-------------------------------: | :-----------------: | :---------: |
| Local Auth | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
| LDAP-based Auth | ✓ | ✓ | ✓ | partial | ✗ | ✓ |
| Content Trust and Validation | ✓ | ✓ | ✗ | ✗ | partial | partial |
| Vulnerability Scanning & Monitoring | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |
| Replication | ✓ | ✓ | ✓ | n/a | ✗ | ✓ |
| Multi-Tenancy (projects, teams, etc.) | ✓ | ✓ | ✓ | partial | ✗ | ✓ |
| Role-Based Access Control | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
| Custom TLS Certificates | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
| Ability to Determine Version of Binaries in Containers | ✓ | ✓ | ✓ | ✗ | ✗ | ? |
| Upstream Registry Proxy Cache | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ |
| Audit Logs | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
Table updated on 10/21/2019 against Harbor 1.9.
| Feature | Harbor | Docker Trusted Registry | Quay | Cloud Providers (GCP, AWS, Azure) | Docker Distribution | Artifactory | GitLab |
| -------------: | :----: | :---------------------: | :-----: | :-------------------------------: | :-----------------: | :---------: | :------: |
| Ability to Determine Version of Binaries in Containers | ✓ | ✓ | ✓ | ✗ | ✗ | ? | ? |
| Artifact Repository (rpms, git, jar, etc) | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | partial |
| Audit Logs | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
| Content Trust and Validation | ✓ | ✓ | ✗ | ✗ | partial | partial | ✗ |
| Custom TLS Certificates | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| Helm Chart Repository Manager | ✓ | ✗ | partial | ✗ | ✗ | ✓ | ✗ |
| LDAP-based Auth | ✓ | ✓ | ✓ | partial | ✗ | ✓ | ✓ |
| Local Auth | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
| Multi-Tenancy (projects, teams, namespaces, etc) | ✓ | ✓ | ✓ | partial | ✗ | ✓ | ✓ |
| Open Source | ✓ | partial | ✗ | ✗ | ✓ | partial | partial |
| Project Quotas (by image count & storage consumption) | ✓ | ✗ | ✗ | partial | ✗ | ✗ | ✗ |
| Replication between instances | ✓ | ✓ | ✓ | n/a | ✗ | ✓ | ✗ |
| Replication between non-instances | ✓ | ✗ | ✓ | n/a | ✗ | ✗ | ✗ |
| Robot Accounts for Helm Charts | ✓ | ✗ | ✗ | ? | ✗ | ✗ | ✗ |
| Robot Accounts for Images | ✓ | ? | ✓ | ? | ✗ | ? | ? |
| Role-Based Access Control | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✗ |
| Single Sign On (OIDC) | ✓ | ✓ | ✓ | ✓ | ✗ | partial | ✗ |
| Tag Retention Policy | ✓ | ✗ | partial | ✗ | ✗ | ✗ | ✗ |
| Upstream Registry Proxy Cache | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
| Vulnerability Scanning & Monitoring | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | partial |
| Vulnerability Scanning Plugin Framework | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Vulnerability Whitelisting | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Webhooks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |

View File

@ -1089,6 +1089,8 @@ paths:
description: Forbidden.
'404':
description: Repository not found.
'412':
description: Precondition Failed.
put:
summary: Update description of the repository.
description: |

View File

@ -36,10 +36,10 @@ version | set harbor version
#### EXAMPLE:
#### Build and run harbor from source code.
make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true
### Package offline installer
make package_offline GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
make package_offline GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true
### Start harbor with notary
make -e NOTARYFLAG=true start

View File

@ -97,7 +97,7 @@ DOCKERIMAGENAME_MIGRATOR=goharbor/harbor-migrator
# for chart server (chartmuseum)
DOCKERFILEPATH_CHART_SERVER=$(DOCKERFILEPATH)/chartserver
DOCKERFILENAME_CHART_SERVER=Dockerfile
CHART_SERVER_CODE_BASE=github.com/helm/chartmuseum
CHART_SERVER_CODE_BASE=https://github.com/helm/chartmuseum.git
CHART_SERVER_MAIN_PATH=cmd/chartmuseum
CHART_SERVER_BIN_NAME=chartm
@ -195,7 +195,7 @@ _build_registry:
rm -rf $(DOCKERFILEPATH_REG)/binary && mkdir -p $(DOCKERFILEPATH_REG)/binary && \
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/registry/release-$(REGISTRYVERSION)/registry, $(DOCKERFILEPATH_REG)/binary/registry); \
else \
cd $(DOCKERFILEPATH_REG) && $(DOCKERFILEPATH_REG)/builder $(REGISTRYVERSION) && cd - ; \
cd $(DOCKERFILEPATH_REG) && $(DOCKERFILEPATH_REG)/builder $(REGISTRY_SRC_TAG) && cd - ; \
fi
@echo "building registry container for photon..."
@chmod 655 $(DOCKERFILEPATH_REG)/binary/registry && $(DOCKERBUILD) -f $(DOCKERFILEPATH_REG)/$(DOCKERFILENAME_REG) -t $(DOCKERIMAGENAME_REG):$(REGISTRYVERSION)-$(VERSIONTAG) .

View File

@ -30,4 +30,4 @@ cp compile.sh binary/
docker run -it --rm -v $cur/binary:/go/bin --name golang_code_builder $GOLANG_IMAGE /bin/bash /go/bin/compile.sh $GIT_PATH $CODE_VERSION $MAIN_GO_PATH $BIN_NAME
#Clear
docker rm -f golang_code_builder
#docker rm -f golang_code_builder

View File

@ -1,4 +1,4 @@
FROM golang:1.12.5
FROM golang:1.12.12
ADD . /go/src/github.com/goharbor/harbor-scanner-clair/
WORKDIR /go/src/github.com/goharbor/harbor-scanner-clair/

View File

@ -23,7 +23,7 @@ TEMP=`mktemp -d ${TMPDIR-/tmp}/clair-adapter.XXXXXX`
git clone https://github.com/danielpacak/harbor-scanner-clair.git $TEMP
cd $TEMP; git checkout $VERSION; cd -
echo 'build the clair adapter binary bases on the golang:1.12.5...'
echo 'build the clair adapter binary bases on the golang:1.12.12'
cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t clair-adapter-golang $TEMP

View File

@ -1,4 +1,4 @@
FROM golang:1.11.2
FROM golang:1.12.12
ADD . /go/src/github.com/coreos/clair/
WORKDIR /go/src/github.com/coreos/clair/

View File

@ -23,7 +23,7 @@ TEMP=`mktemp -d /$TMPDIR/clair.XXXXXX`
git clone https://github.com/coreos/clair.git $TEMP
cd $TEMP; git checkout $VERSION; cd -
echo 'build the clair binary bases on the golang:1.11.2...'
echo 'build the clair binary bases on the golang:1.12.12'
cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t clair-golang $TEMP

View File

@ -1,4 +1,4 @@
FROM golang:1.11.2
FROM golang:1.12.12
ARG NOTARY_VERSION
ARG MIGRATE_VERSION

View File

@ -24,6 +24,8 @@ docker cp $ID:/go/bin/notary-signer binary/
docker cp $ID:/go/bin/migrate binary/
docker cp $ID:/migrations binary/
sed -i 's/waiting for $DB_URL/waiting for database/g' binary/migrations/migrate.sh
docker rm -f $ID
docker rmi -f notary-binary

View File

@ -1,4 +1,4 @@
FROM golang:1.11
FROM golang:1.12.12
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV BUILDTAGS include_oss include_gcs

View File

@ -29,7 +29,7 @@ wget https://github.com/docker/distribution/pull/2879.patch
git apply 2879.patch
cd $cur
echo 'build the registry binary bases on the golang:1.11...'
echo 'build the registry binary ...'
cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t registry-golang $TEMP

View File

@ -18,6 +18,7 @@ package metadata
import (
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
@ -139,12 +140,12 @@ type Int64Type struct {
}
func (t *Int64Type) validate(str string) error {
_, err := strconv.ParseInt(str, 10, 64)
_, err := parseInt64(str)
return err
}
func (t *Int64Type) get(str string) (interface{}, error) {
return strconv.ParseInt(str, 10, 64)
return parseInt64(str)
}
// BoolType ...
@ -194,7 +195,7 @@ type QuotaType struct {
}
func (t *QuotaType) validate(str string) error {
val, err := strconv.ParseInt(str, 10, 64)
val, err := parseInt64(str)
if err != nil {
return err
}
@ -205,3 +206,18 @@ func (t *QuotaType) validate(str string) error {
return nil
}
// parseInt64 returns int64 from string which support scientific notation
func parseInt64(str string) (int64, error) {
val, err := strconv.ParseInt(str, 10, 64)
if err == nil {
return val, nil
}
fval, err := strconv.ParseFloat(str, 64)
if err == nil && fval == math.Trunc(fval) {
return int64(fval), nil
}
return 0, fmt.Errorf("invalid int64 string: %s", str)
}

View File

@ -15,8 +15,9 @@
package metadata
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIntType_validate(t *testing.T) {
@ -96,3 +97,33 @@ func TestMapType_get(t *testing.T) {
result, _ := test.get(`{"sample":"abc", "another":"welcome"}`)
assert.Equal(t, map[string]interface{}{"sample": "abc", "another": "welcome"}, result)
}
func Test_parseInt64(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want int64
wantErr bool
}{
{"1", args{"1"}, int64(1), false},
{"1.0", args{"1.0"}, int64(1), false},
{"1.1", args{"1.1"}, int64(0), true},
{"1E2", args{"1E2"}, int64(100), false},
{"1.073741824e+11", args{"1.073741824e+11"}, int64(107374182400), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseInt64(tt.args.str)
if (err != nil) != tt.wantErr {
t.Errorf("parseInt64() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseInt64() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -73,6 +73,7 @@ type TagDetail struct {
Author string `json:"author"`
Created time.Time `json:"created"`
Config *TagCfg `json:"config"`
Immutable bool `json:"immutable"`
}
// TagCfg ...

View File

@ -10,7 +10,10 @@ import (
"mime/multipart"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/goharbor/harbor/src/chartserver"
"github.com/goharbor/harbor/src/common"
@ -18,14 +21,10 @@ import (
hlog "github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/label"
"github.com/goharbor/harbor/src/core/middlewares"
n_event "github.com/goharbor/harbor/src/core/notifier/event"
rep_event "github.com/goharbor/harbor/src/replication/event"
"github.com/goharbor/harbor/src/replication/model"
"path"
"strconv"
"time"
)
const (
@ -489,6 +488,12 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R
extInfo["operator"] = cra.SecurityCtx.GetUsername()
extInfo["projectName"] = cra.namespace
extInfo["chartName"] = chartDetails.Metadata.Name
public, err := cra.ProjectMgr.IsPublic(cra.namespace)
if err != nil {
hlog.Errorf("failed to check the public of project %s: %v", cra.namespace, err)
public = false
}
e := &rep_event.Event{
Type: rep_event.EventTypeChartUpload,
Resource: &model.Resource{
@ -496,6 +501,9 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R
Metadata: &model.ResourceMetadata{
Repository: &model.Repository{
Name: fmt.Sprintf("%s/%s", cra.namespace, chartDetails.Metadata.Name),
Metadata: map[string]interface{}{
"public": strconv.FormatBool(public),
},
},
Vtags: []string{chartDetails.Metadata.Version},
},

View File

@ -28,7 +28,7 @@ import (
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
@ -45,6 +45,8 @@ import (
"github.com/goharbor/harbor/src/core/config"
notifierEvt "github.com/goharbor/harbor/src/core/notifier/event"
coreutils "github.com/goharbor/harbor/src/core/utils"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/replication/event"
"github.com/goharbor/harbor/src/replication/model"
@ -283,11 +285,6 @@ func (ra *RepositoryAPI) Delete() {
}
for _, t := range tags {
image := fmt.Sprintf("%s:%s", repoName, t)
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
ra.SendInternalServerError(fmt.Errorf("failed to delete labels of image %s: %v", image, err))
return
}
if err = rc.DeleteTag(t); err != nil {
if regErr, ok := err.(*commonhttp.Error); ok {
if regErr.Code == http.StatusNotFound {
@ -298,6 +295,11 @@ func (ra *RepositoryAPI) Delete() {
return
}
log.Infof("delete tag: %s:%s", repoName, t)
image := fmt.Sprintf("%s:%s", repoName, t)
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
ra.SendInternalServerError(fmt.Errorf("failed to delete labels of image %s: %v", image, err))
return
}
go func(tag string) {
e := &event.Event{
@ -711,6 +713,9 @@ func assembleTag(c chan *models.TagResp, client *registry.Repository, projectID
}
}
// get immutable status
item.Immutable = isImmutable(projectID, repository, tag)
c <- item
}
@ -791,6 +796,21 @@ func populateAuthor(detail *models.TagDetail) {
}
}
// check whether the tag is immutable
func isImmutable(projectID int64, repo string, tag string) bool {
_, repoName := utils.ParseRepository(repo)
matched, err := rule.NewRuleMatcher(projectID).Match(art.Candidate{
Repository: repoName,
Tag: tag,
NamespaceID: projectID,
})
if err != nil {
log.Error(err)
return false
}
return matched
}
// GetManifests returns the manifest of a tag
func (ra *RepositoryAPI) GetManifests() {
repoName := ra.GetString(":splat")

View File

@ -35,4 +35,4 @@ var ChartMiddlewares = []string{CHART}
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
// MiddlewaresLocal ...
var MiddlewaresLocal = []string{SIZEQUOTA, COUNTQUOTA}
var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA}

View File

@ -0,0 +1,54 @@
package immutable
import (
"fmt"
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
"github.com/goharbor/harbor/src/core/middlewares/interceptor/immutable"
"github.com/goharbor/harbor/src/core/middlewares/util"
"net/http"
)
var (
defaultBuilders = []interceptor.Builder{
&manifestDeletionBuilder{},
&manifestCreationBuilder{},
}
)
type manifestDeletionBuilder struct{}
func (*manifestDeletionBuilder) Build(req *http.Request) (interceptor.Interceptor, error) {
if match, _, _ := util.MatchDeleteManifest(req); !match {
return nil, nil
}
info, ok := util.ManifestInfoFromContext(req.Context())
if !ok {
var err error
info, err = util.ParseManifestInfoFromPath(req)
if err != nil {
return nil, fmt.Errorf("failed to parse manifest, error %v", err)
}
}
return immutable.NewDeleteMFInteceptor(info), nil
}
type manifestCreationBuilder struct{}
func (*manifestCreationBuilder) Build(req *http.Request) (interceptor.Interceptor, error) {
if match, _, _ := util.MatchPushManifest(req); !match {
return nil, nil
}
info, ok := util.ManifestInfoFromContext(req.Context())
if !ok {
var err error
info, err = util.ParseManifestInfoFromReq(req)
if err != nil {
return nil, fmt.Errorf("failed to parse manifest, error %v", err)
}
}
return immutable.NewPushMFInteceptor(info), nil
}

View File

@ -16,78 +16,74 @@ package immutable
import (
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
common_util "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
"github.com/goharbor/harbor/src/core/middlewares/util"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
"net/http"
)
type immutableHandler struct {
builders []interceptor.Builder
next http.Handler
}
// New ...
func New(next http.Handler) http.Handler {
func New(next http.Handler, builders ...interceptor.Builder) http.Handler {
if len(builders) == 0 {
builders = defaultBuilders
}
return &immutableHandler{
builders: builders,
next: next,
}
}
// ServeHTTP ...
func (rh immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if match, _, _ := util.MatchPushManifest(req); !match {
rh.next.ServeHTTP(rw, req)
return
}
info, ok := util.ManifestInfoFromContext(req.Context())
if !ok {
var err error
info, err = util.ParseManifestInfoFromPath(req)
func (rh *immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
interceptor, err := rh.getInterceptor(req)
if err != nil {
log.Error(err)
rh.next.ServeHTTP(rw, req)
log.Warningf("Error occurred when to handle request in immutable handler: %v", err)
http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in immutable handler: %v", err)),
http.StatusInternalServerError)
return
}
}
_, repoName := common_util.ParseRepository(info.Repository)
matched, err := rule.NewRuleMatcher(info.ProjectID).Match(art.Candidate{
Repository: repoName,
Tag: info.Tag,
NamespaceID: info.ProjectID,
})
if err != nil {
log.Error(err)
rh.next.ServeHTTP(rw, req)
return
}
if !matched {
if interceptor == nil {
rh.next.ServeHTTP(rw, req)
return
}
artifactQuery := &models.ArtifactQuery{
PID: info.ProjectID,
Repo: info.Repository,
Tag: info.Tag,
}
afs, err := dao.ListArtifacts(artifactQuery)
if err != nil {
log.Error(err)
rh.next.ServeHTTP(rw, req)
return
}
if len(afs) == 0 {
rh.next.ServeHTTP(rw, req)
return
}
// rule matched and non-existent is a immutable tag
if err := interceptor.HandleRequest(req); err != nil {
log.Warningf("Error occurred when to handle request in immutable handler: %v", err)
if _, ok := err.(middlerware_err.ErrImmutable); ok {
http.Error(rw, util.MarshalError("DENIED",
fmt.Sprintf("The tag:%s:%s is immutable, cannot be overwrite.", info.Repository, info.Tag)), http.StatusPreconditionFailed)
fmt.Sprintf("The tag is immutable, cannot be overwrite: %v", err)), http.StatusPreconditionFailed)
return
}
http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in immutable handler: %v", err)),
http.StatusInternalServerError)
return
}
rh.next.ServeHTTP(rw, req)
interceptor.HandleResponse(rw, req)
}
func (rh *immutableHandler) getInterceptor(req *http.Request) (interceptor.Interceptor, error) {
for _, builder := range rh.builders {
interceptor, err := builder.Build(req)
if err != nil {
return nil, err
}
if interceptor != nil {
return interceptor, nil
}
}
return nil, nil
}

View File

@ -0,0 +1,67 @@
package immutable
import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
common_util "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
"github.com/goharbor/harbor/src/core/middlewares/util"
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"net/http"
)
// NewDeleteMFInteceptor ....
func NewDeleteMFInteceptor(mf *util.ManifestInfo) interceptor.Interceptor {
return &delmfInterceptor{
mf: mf,
}
}
type delmfInterceptor struct {
mf *util.ManifestInfo
}
// HandleRequest ...
func (dmf *delmfInterceptor) HandleRequest(req *http.Request) (err error) {
artifactQuery := &models.ArtifactQuery{
Digest: dmf.mf.Digest,
Repo: dmf.mf.Repository,
PID: dmf.mf.ProjectID,
}
var afs []*models.Artifact
afs, err = dao.ListArtifacts(artifactQuery)
if err != nil {
log.Error(err)
return
}
if len(afs) == 0 {
return
}
for _, af := range afs {
_, repoName := common_util.ParseRepository(dmf.mf.Repository)
var matched bool
matched, err = rule.NewRuleMatcher(dmf.mf.ProjectID).Match(art.Candidate{
Repository: repoName,
Tag: af.Tag,
NamespaceID: dmf.mf.ProjectID,
})
if err != nil {
log.Error(err)
return
}
if matched {
return middlerware_err.NewErrImmutable(repoName)
}
}
return
}
// HandleRequest ...
func (dmf *delmfInterceptor) HandleResponse(w http.ResponseWriter, r *http.Request) {
}

View File

@ -0,0 +1,65 @@
package immutable
import (
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
common_util "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
"github.com/goharbor/harbor/src/core/middlewares/util"
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"net/http"
)
// NewPushMFInteceptor ....
func NewPushMFInteceptor(mf *util.ManifestInfo) interceptor.Interceptor {
return &pushmfInterceptor{
mf: mf,
}
}
type pushmfInterceptor struct {
mf *util.ManifestInfo
}
// HandleRequest ...
func (pmf *pushmfInterceptor) HandleRequest(req *http.Request) (err error) {
_, repoName := common_util.ParseRepository(pmf.mf.Repository)
var matched bool
matched, err = rule.NewRuleMatcher(pmf.mf.ProjectID).Match(art.Candidate{
Repository: repoName,
Tag: pmf.mf.Tag,
NamespaceID: pmf.mf.ProjectID,
})
if err != nil {
log.Error(err)
return
}
if !matched {
return
}
artifactQuery := &models.ArtifactQuery{
PID: pmf.mf.ProjectID,
Repo: pmf.mf.Repository,
Tag: pmf.mf.Tag,
}
var afs []*models.Artifact
afs, err = dao.ListArtifacts(artifactQuery)
if err != nil {
log.Error(err)
return
}
if len(afs) == 0 {
return
}
return middlerware_err.NewErrImmutable(repoName)
}
// HandleRequest ...
func (pmf *pushmfInterceptor) HandleResponse(w http.ResponseWriter, r *http.Request) {
}

View File

@ -0,0 +1,20 @@
package error
import (
"fmt"
)
// ErrImmutable ...
type ErrImmutable struct {
repo string
}
// Error ...
func (ei ErrImmutable) Error() string {
return fmt.Sprintf("Failed to process request, due to immutable. '%s'", ei.repo)
}
// NewErrImmutable ...
func NewErrImmutable(msg string) ErrImmutable {
return ErrImmutable{repo: msg}
}

View File

@ -17,6 +17,7 @@ package registry
import (
"encoding/json"
"regexp"
"strconv"
"strings"
"time"
@ -140,7 +141,6 @@ func (n *NotificationHandler) Post() {
log.Errorf("failed to build image push event metadata: %v", err)
}
// TODO: handle image delete event and chart event
go func() {
e := &rep_event.Event{
Type: rep_event.EventTypeImagePush,
@ -149,7 +149,9 @@ func (n *NotificationHandler) Post() {
Metadata: &model.ResourceMetadata{
Repository: &model.Repository{
Name: repository,
// TODO filling the metadata
Metadata: map[string]interface{}{
"public": strconv.FormatBool(pro.IsPublic()),
},
},
Vtags: []string{tag},
},

View File

@ -17,6 +17,7 @@ package gc
import (
"fmt"
"os"
"strconv"
"time"
"github.com/garyburd/redigo/redis"
@ -29,7 +30,6 @@ import (
"github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/types"
"github.com/goharbor/harbor/src/registryctl/client"
"strconv"
)
const (
@ -38,6 +38,7 @@ const (
dialWriteTimeout = 10 * time.Second
blobPrefix = "blobs::*"
repoPrefix = "repository::*"
uploadSizePattern = "upload:*:size"
)
// GarbageCollector is the struct to run registry's garbage collection
@ -156,15 +157,13 @@ func (gc *GarbageCollector) cleanCache() error {
// sample of keys in registry redis:
// 1) "blobs::sha256:1a6fd470b9ce10849be79e99529a88371dff60c60aab424c077007f6979b4812"
// 2) "repository::library/hello-world::blobs::sha256:4ab4c602aa5eed5528a6620ff18a1dc4faef0e1ab3a5eddeddb410714478c67f"
err = delKeys(con, blobPrefix)
if err != nil {
gc.logger.Errorf("failed to clean registry cache %v, pattern blobs::*", err)
// 3) "upload:fbd2e0a3-262d-40bb-abe4-2f43aa6f9cda:size"
patterns := []string{blobPrefix, repoPrefix, uploadSizePattern}
for _, pattern := range patterns {
if err := delKeys(con, pattern); err != nil {
gc.logger.Errorf("failed to clean registry cache %v, pattern %s", err, pattern)
return err
}
err = delKeys(con, repoPrefix)
if err != nil {
gc.logger.Errorf("failed to clean registry cache %v, pattern repository::*", err)
return err
}
return nil

View File

@ -209,6 +209,10 @@
</clr-tooltip>
</label>
</div>
<div class="clr-checkbox-wrapper clr-form-control" [hidden]="policyId < 0">
<input type="checkbox" [checked]="true" id="enablePolicy" formControlName="enabled" class="clr-checkbox">
<label for="enablePolicy" class="clr-control-label">{{'REPLICATION.ENABLED_RULE' | translate}}</label>
</div>
</div>
</div>
<div class="loading-center">

View File

@ -203,6 +203,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
})
}),
filters: this.fb.array([]),
enabled: true,
deletion: false,
override: true
});
@ -228,6 +229,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
},
deletion: false,
enabled: true,
override: true
});
this.isPushMode = true;
@ -251,6 +253,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
dest_registry: rule.dest_registry,
trigger: rule.trigger,
deletion: rule.deletion,
enabled: rule.enabled,
override: rule.override
});
let filtersArray = this.getFilterArray(rule);

View File

@ -24,7 +24,7 @@
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between jobsRow">
<h5 class="flex-items-xs-bottom option-left-down">{{'REPLICATION.REPLICATION_EXECUTIONS' | translate}}</h5>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div class="row flex-items-xs-between flex-items-xs-bottom fiter-task">
<div class="execution-select">
<div class="select filter-tag" [hidden]="!isOpenFilterTag">
<select (change)="doFilterJob($event)">

View File

@ -49,7 +49,10 @@
.row-right {
margin-left: 564px;
}
.fiter-task {
margin-left: .4rem;
margin-top: .05rem;
}
.replication-row {
position: relative;
}

View File

@ -1,5 +1,4 @@
import { ComponentFixture, TestBed, async, } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement} from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
@ -25,7 +24,13 @@ import { ChannelService } from '../channel/index';
import { LabelPieceComponent } from "../label-piece/label-piece.component";
import { LabelDefaultService, LabelService } from "../service/label.service";
import { OperationService } from "../operation/operation.service";
import { ProjectDefaultService, ProjectService, RetagDefaultService, RetagService } from "../service";
import {
ProjectDefaultService,
ProjectService,
RetagDefaultService,
RetagService, ScanningResultDefaultService,
ScanningResultService
} from "../service";
import { UserPermissionDefaultService, UserPermissionService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { of } from "rxjs";
@ -158,6 +163,17 @@ describe('RepositoryComponent (inline template)', () => {
let mockHasRetagImagePermission: boolean = true;
let mockHasDeleteImagePermission: boolean = true;
let mockHasScanImagePermission: boolean = true;
let fakedScanningResultService = {
getProjectScanner() {
return of({});
}
};
const permissions = [
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
];
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
@ -188,7 +204,8 @@ describe('RepositoryComponent (inline template)', () => {
{ provide: LabelService, useClass: LabelDefaultService},
{ provide: UserPermissionService, useClass: UserPermissionDefaultService},
{ provide: ChannelService},
{ provide: OperationService }
{ provide: OperationService },
{ provide: ScanningResultService, useValue: fakedScanningResultService }
]
});
}));
@ -213,16 +230,10 @@ describe('RepositoryComponent (inline template)', () => {
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(of(mockLabels).pipe(delay(0)));
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(of(mockLabels1).pipe(delay(0)));
spyOn(userPermissionService, "getPermission")
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE )
.and.returnValue(of(mockHasAddLabelImagePermission))
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL )
.and.returnValue(of(mockHasRetagImagePermission))
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE )
.and.returnValue(of(mockHasDeleteImagePermission))
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY
, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE)
.and.returnValue(of(mockHasScanImagePermission));
spyOn(userPermissionService, "hasProjectPermissions")
.withArgs(compRepo.projectId, permissions )
.and.returnValue(of([mockHasAddLabelImagePermission, mockHasRetagImagePermission,
mockHasDeleteImagePermission, mockHasScanImagePermission]));
fixture.detectChanges();
});
let originalTimeout;

View File

@ -68,6 +68,7 @@ export interface Tag extends Base {
labels: Label[];
push_time?: string;
pull_time?: string;
immutable?: boolean;
}
/**
@ -314,6 +315,7 @@ export interface VulnerabilitySummary {
}
export interface SeveritySummary {
total: number;
fixable: number;
summary: {[key: string]: number};
}

View File

@ -73,6 +73,19 @@ export abstract class ScanningResultService {
* @memberOf ScanningResultService
*/
abstract startScanningAll(): Observable<any>;
/**
* Get scanner metadata
* @param uuid
* @memberOf ScanningResultService
*/
abstract getScannerMetadata(uuid: string): Observable<any>;
/**
* Get project scanner
* @param projectId
*/
abstract getProjectScanner(projectId: number): Observable<any>;
}
@Injectable()
@ -153,4 +166,14 @@ export class ScanningResultDefaultService extends ScanningResultService {
})
, catchError(error => observableThrowError(error)));
}
getScannerMetadata(uuid: string): Observable<any> {
return this.http.get(`/api/scanners/${uuid}/metadata`)
.pipe(map(response => response as any))
.pipe(catchError(error => observableThrowError(error)));
}
getProjectScanner(projectId: number): Observable<any> {
return this.http.get(`/api/projects/${projectId}/scanner`)
.pipe(map(response => response as any))
.pipe(catchError(error => observableThrowError(error)));
}
}

View File

@ -39,11 +39,11 @@
</section>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="vulnerability" [hidden]="hasCve">
<div class="col-md-4 col-sm-6 margin-top-5px">
<div class="vulnerability" [hidden]="hasCve || showStatBar">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="vulnerabilitySummary"></hbr-vulnerability-bar>
</div>
<histogram-chart *ngIf="hasCve" class="margin-top-5px" [metadata]="passMetadataToChart()" [isWhiteBackground]="true"></histogram-chart>
<histogram-chart *ngIf="hasCve" [metadata]="passMetadataToChart()" [isWhiteBackground]="true"></histogram-chart>
</div>
<div *ngIf="!withAdmiral && tagDetails?.labels?.length">
<div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div>

View File

@ -48,6 +48,7 @@ describe("TagDetailComponent (inline template)", () => {
end_time: new Date(),
summary: {
total: 124,
fixable: 50,
summary: {
"High": 5,
"Low": 5

View File

@ -56,6 +56,7 @@ export class TagDetailComponent implements OnInit {
hasVulnerabilitiesListPermission: boolean;
hasBuildHistoryPermission: boolean;
@Input() projectId: number;
showStatBar: boolean = true;
constructor(
private tagService: TagService,
public channel: ChannelService,
@ -83,6 +84,7 @@ export class TagDetailComponent implements OnInit {
&& tagDetails.scan_overview
&& tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]) {
this.vulnerabilitySummary = tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE];
this.showStatBar = false;
}
}
onBack(): void {

View File

@ -93,6 +93,7 @@
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-cell class="truncated flex-max-width">
<a href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
<span *ngIf="t.immutable" class="label label-info ml-1">{{'REPOSITORY.IMMUTABLE' | translate}}</span>
</clr-dg-cell>
<clr-dg-cell>{{sizeTransform(t.size)}}</clr-dg-cell>
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">

View File

@ -112,6 +112,15 @@ describe("TagComponent (inline template)", () => {
let mockHasRetagImagePermission: boolean = true;
let mockHasDeleteImagePermission: boolean = true;
let mockHasScanImagePermission: boolean = true;
const mockErrorHandler = {
error: () => {}
};
const permissions = [
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
];
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
@ -141,6 +150,7 @@ describe("TagComponent (inline template)", () => {
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
{ provide: LabelService, useClass: LabelDefaultService },
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
{ provide: mockErrorHandler, useValue: ErrorHandler },
{ provide: OperationService },
]
}).compileComponents();
@ -169,15 +179,10 @@ describe("TagComponent (inline template)", () => {
let http: HttpClient;
http = fixture.debugElement.injector.get(HttpClient);
spyScanner = spyOn(http, "get").and.returnValue(of(scannerMock));
spyOn(userPermissionService, "getPermission")
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE )
.and.returnValue(of(mockHasAddLabelImagePermission))
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL )
.and.returnValue(of(mockHasRetagImagePermission))
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE )
.and.returnValue(of(mockHasDeleteImagePermission))
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE)
.and.returnValue(of(mockHasScanImagePermission));
spyOn(userPermissionService, "hasProjectPermissions")
.withArgs(comp.projectId, permissions )
.and.returnValue(of([mockHasAddLabelImagePermission, mockHasRetagImagePermission,
mockHasDeleteImagePermission, mockHasScanImagePermission]));
labelService = fixture.debugElement.injector.get(LabelService);

View File

@ -27,7 +27,12 @@ import { catchError, debounceTime, distinctUntilChanged, finalize, map } from 'r
import { TranslateService } from "@ngx-translate/core";
import { Comparator, Label, State, Tag, TagClickEvent } from "../service/interface";
import { RequestQueryParams, RetagService, TagService, VulnerabilitySeverity } from "../service/index";
import {
RequestQueryParams,
RetagService,
ScanningResultService,
TagService,
} from "../service/index";
import { ErrorHandler } from "../error-handler/error-handler";
import { ChannelService } from "../channel/index";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../shared/shared.const";
@ -54,7 +59,6 @@ import { operateChanges, OperateInfo, OperationState } from "../operation/operat
import { OperationService } from "../operation/operation.service";
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
import { errorHandler as errorHandFn } from "../shared/shared.utils";
import { HttpClient } from "@angular/common/http";
import { ClrLoadingState } from "@clr/angular";
export interface LabelState {
@ -160,7 +164,7 @@ export class TagComponent implements OnInit, AfterViewInit {
private ref: ChangeDetectorRef,
private operationService: OperationService,
private channel: ChannelService,
private http: HttpClient
private scanningService: ScanningResultService
) { }
ngOnInit() {
@ -220,9 +224,6 @@ export class TagComponent implements OnInit, AfterViewInit {
}
ngAfterViewInit() {
if (!this.withAdmiral) {
this.getAllLabels();
}
}
public get filterLabelPieceWidth() {
@ -726,20 +727,23 @@ export class TagComponent implements OnInit, AfterViewInit {
return st !== VULNERABILITY_SCAN_STATUS.RUNNING;
}
getImagePermissionRule(projectId: number): void {
let hasAddLabelImagePermission = this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY,
USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE);
let hasRetagImagePermission = this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL);
let hasDeleteImagePermission = this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE);
let hasScanImagePermission = this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE);
forkJoin(hasAddLabelImagePermission, hasRetagImagePermission, hasDeleteImagePermission, hasScanImagePermission)
.subscribe(permissions => {
this.hasAddLabelImagePermission = permissions[0] as boolean;
this.hasRetagImagePermission = permissions[1] as boolean;
this.hasDeleteImagePermission = permissions[2] as boolean;
this.hasScanImagePermission = permissions[3] as boolean;
const permissions = [
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
];
this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => {
this.hasAddLabelImagePermission = results[0];
this.hasRetagImagePermission = results[1];
this.hasDeleteImagePermission = results[2];
this.hasScanImagePermission = results[3];
// only has label permission
if (this.hasAddLabelImagePermission) {
if (!this.withAdmiral) {
this.getAllLabels();
}
}
}, error => this.errorHandler.error(error));
}
// Trigger scan
@ -759,14 +763,22 @@ export class TagComponent implements OnInit, AfterViewInit {
getProjectScanner(): void {
this.hasEnabledScanner = false;
this.scanBtnState = ClrLoadingState.LOADING;
this.http.get(`/api/projects/${this.projectId}/scanner`)
.pipe(map(response => response as any))
.pipe(catchError(error => observableThrowError(error)))
this.scanningService.getProjectScanner(this.projectId)
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disable
&& response.health) {
this.hasEnabledScanner = true;
if (response && "{}" !== JSON.stringify(response) && !response.disabled
&& response.uuid) {
this.getScannerMetadata(response.uuid);
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
getScannerMetadata(uuid: string) {
this.scanningService.getScannerMetadata(uuid)
.subscribe(response => {
this.hasEnabledScanner = true;
this.scanBtnState = ClrLoadingState.SUCCESS;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;

View File

@ -127,6 +127,6 @@ export class HistogramChartComponent implements OnInit, AfterViewInit, DoCheck {
});
}
this.max = count;
this.scale = Math.ceil(count / 4);
this.scale = Math.ceil(count / 40) * 10;
}
}

View File

@ -32,6 +32,7 @@ describe('ResultBarChartComponent (inline template)', () => {
end_time: new Date(),
summary: {
total: 124,
fixable: 50,
summary: {
"High": 5,
"Low": 5

View File

@ -10,7 +10,7 @@
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!hasScanImagePermission" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [clrLoading]="scanBtnState" [disabled]="!hasScanImagePermission || !hasEnabledScanner" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>

View File

@ -10,7 +10,8 @@ import { ChannelService } from "../channel/channel.service";
import { UserPermissionService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static";
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from '../utils';
import { finalize } from "rxjs/operators";
import { finalize, map } from "rxjs/operators";
import { ClrLoadingState } from "@clr/angular";
@Component({
@ -22,10 +23,13 @@ export class ResultGridComponent implements OnInit {
scanningResults: VulnerabilityItem[] = [];
dataCache: VulnerabilityItem[] = [];
loading: boolean = false;
shouldShowLoading: boolean = true;
@Input() tagId: string;
@Input() repositoryId: string;
@Input() projectId: number;
hasScanImagePermission: boolean;
hasEnabledScanner: boolean = false;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
constructor(
private scanningService: ScanningResultService,
private channel: ChannelService,
@ -39,11 +43,41 @@ export class ResultGridComponent implements OnInit {
this.channel.tagDetail$.subscribe(tag => {
this.loadResults(this.repositoryId, this.tagId);
});
if (this.projectId) {
this.getProjectScanner();
}
}
getProjectScanner(): void {
this.hasEnabledScanner = false;
this.scanBtnState = ClrLoadingState.LOADING;
this.scanningService.getProjectScanner(this.projectId)
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disabled
&& response.uuid) {
this.getScannerMetadata(response.uuid);
} else {
this.scanBtnState = ClrLoadingState.ERROR;
}
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
getScannerMetadata(uuid: string) {
this.scanningService.getScannerMetadata(uuid)
.subscribe(response => {
this.hasEnabledScanner = true;
this.scanBtnState = ClrLoadingState.SUCCESS;
}, error => {
this.scanBtnState = ClrLoadingState.ERROR;
});
}
loadResults(repositoryId: string, tagId: string): void {
// only show loading for one time
if (this.shouldShowLoading) {
this.loading = true;
this.shouldShowLoading = false;
}
this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId)
.pipe(finalize(() => this.loading = false))
.subscribe((results) => {

View File

@ -1,14 +1,15 @@
<div class="tip-wrapper tip-position width-210">
<clr-tooltip>
<div clrTooltipTrigger class="tip-block">
<ng-container *ngIf="!isNone">
<div *ngIf="criticalCount > 0" class="tip-wrapper bar-block-critical shadow-critical width-30">{{criticalCount}}</div>
<div *ngIf="highCount > 0" class="margin-left-5 tip-wrapper bar-block-high shadow-high width-30">{{highCount}}</div>
<div *ngIf="mediumCount > 0" class="margin-left-5 tip-wrapper bar-block-medium shadow-medium width-30">{{mediumCount}}</div>
<div *ngIf="lowCount > 0" class="margin-left-5 tip-wrapper bar-block-low shadow-low width-30">{{lowCount}}</div>
<div *ngIf="negligibleCount > 0" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-30">{{negligibleCount}}</div>
<div *ngIf="unknownCount > 0" class="margin-left-5 tip-wrapper bar-block-unknown shadow-unknown width-30">{{unknownCount}}</div>
</ng-container>
<div *ngIf="!isNone" class="circle-block">
<div class="level-border" [className]="getClass()">{{vulnerabilitySummary?.severity | slice:0:1}}</div>
<div class="black-point margin-left-5"></div>
<span class="margin-left-5">{{total}}</span>
<span class="margin-left-5">{{'SCANNER.TOTAL' | translate}}</span>
<div class="black-point margin-left-10"></div>
<span class="margin-left-5">{{fixableCount}}</span>
<span class="margin-left-5">{{'SCANNER.FIXABLE' | translate}}</span>
</div>
<div *ngIf="isNone" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-150">{{'VULNERABILITY.NO_VULNERABILITY' | translate }}</div>
</div>
<clr-tooltip-content class="w-800" [clrPosition]="'right'" [clrSize]="'lg'" *clrIfOpen>
@ -46,6 +47,10 @@
<div class="bar-summary bar-tooltip-fon" *ngIf="!isNone">
<histogram-chart [isWhiteBackground]="false" [metadata]="passMetadataToChart()"></histogram-chart>
</div>
<div>
<span class="bar-scanning-time">{{'SCANNER.DURATION' | translate }}</span>
<span class="margin-left-5">{{duration()}}</span>
</div>
<div>
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
<span>{{completeTimestamp | date:'short'}}</span>

View File

@ -208,6 +208,9 @@ hr {
.margin-left-5 {
margin-left: 5px;
}
.margin-left-10 {
margin-left: 10px;
}
.width-30 {
width: 30px;
@ -220,3 +223,53 @@ hr {
.width-150 {
width: 150px;
}
.circle-block {
color: #575757;
display: flex;
align-items: center;
div:first-child {
display: inline-block;
border-radius: 50%;
height: 20px;
width: 20px;
line-height: 20px;
font-size: 14px;
text-align: center;
}
}
.level-border {
border:1px solid #f8b5b4;
}
.level-critical {
background:red;
color:#621501;
}
.level-high {
background:#e64524;
color:#621501;
}
.level-medium {
background-color: orange;
color:#621501;
}
.level-low {
background: #007CBB;
color:#cab6b1;
}
.level-negligible {
background-color: green;
color:#bad7ba;
}
.level-unknown {
background-color: grey;
color:#bad7ba;
}
.black-point {
display: inline-block;
width: 4px;background-color: #000;
height: 4px;
border-radius: 50%;
}

View File

@ -3,6 +3,18 @@ import { VulnerabilitySummary } from "../../service";
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../utils";
import { TranslateService } from "@ngx-translate/core";
const MIN = 60;
const MIN_STR = "min ";
const SEC_STR = "sec";
const CLASS_MAP = {
"CRITICAL": "level-critical",
"HIGH": "level-high",
"MEDIUM": "level-medium",
"LOW": "level-low",
"NEGLIGIBLE": "level-negligible",
"UNKNOWN": "level-unknown"
};
@Component({
selector: 'hbr-result-tip-histogram',
templateUrl: './result-tip-histogram.component.html',
@ -52,7 +64,13 @@ export class ResultTipHistogramComponent implements OnInit {
this.vulnerabilitySummary.summary) {
return this.vulnerabilitySummary.summary.total;
}
return 0;
}
get fixableCount() {
if (this.vulnerabilitySummary &&
this.vulnerabilitySummary.summary && this.vulnerabilitySummary.summary.fixable) {
return this.vulnerabilitySummary.summary.fixable;
}
return 0;
}
@ -72,7 +90,21 @@ export class ResultTipHistogramComponent implements OnInit {
return 0;
}
duration(): string {
if (this.vulnerabilitySummary && this.vulnerabilitySummary.duration) {
let str = '';
const min = Math.floor(this.vulnerabilitySummary.duration / MIN);
if (min) {
str += min + MIN_STR;
}
const sec = this.vulnerabilitySummary.duration % MIN;
if (sec) {
str += sec + SEC_STR;
}
return str;
}
return '0';
}
get highCount(): number {
if (this.sevSummary) {
return this.sevSummary[VULNERABILITY_SEVERITY.HIGH];
@ -139,6 +171,29 @@ export class ResultTipHistogramComponent implements OnInit {
return this.total === 0;
}
getClass(): string {
if (this.vulnerabilitySummary && this.vulnerabilitySummary.severity) {
if (this.isCritical) {
return CLASS_MAP.CRITICAL;
}
if (this.isHigh) {
return CLASS_MAP.HIGH;
}
if (this.isMedium) {
return CLASS_MAP.MEDIUM;
}
if (this.isLow) {
return CLASS_MAP.LOW;
}
if (this.isNegligible) {
return CLASS_MAP.NEGLIGIBLE;
}
if (this.isUnknown) {
return CLASS_MAP.UNKNOWN;
}
}
return null;
}
passMetadataToChart() {
return [
{

View File

@ -20,6 +20,7 @@ describe('ResultTipComponent (inline template)', () => {
end_time: new Date(),
summary: {
total: 124,
fixable: 50,
summary: {
"High": 5,
"Low": 5

View File

@ -19,7 +19,6 @@ clr-modal {
align-items: center;
.reset-cli {
height: 30px;
padding-top: 8px;
}
.btn-padding-less {
padding-left: 5px;

View File

@ -374,6 +374,7 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
}
closeReset() {
this.showSecretDetail = false;
this.showGenerateCliFn();
this.resetSecretFrom.resetForm(new ResetSecret());
}
}

View File

@ -65,10 +65,13 @@
</clr-dg-cell>
<clr-dg-cell>{{scanner.url}}</clr-dg-cell>
<clr-dg-cell>
<span *ngIf="scanner.health;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
<span *ngIf="scanner.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2"></span>
<ng-template #elseBlockLoading>
<span *ngIf="scanner.metadata;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
<ng-template #elseBlock>
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
</ng-template>
</ng-template>
</clr-dg-cell>
<clr-dg-cell>{{!scanner.disabled}}</clr-dg-cell>
<clr-dg-cell>{{scanner.auth?scanner.auth:'None'}}</clr-dg-cell>

View File

@ -57,10 +57,27 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
.pipe(finalize(() => this.onGoing = false))
.subscribe(response => {
this.scanners = response;
this.getMetadataForAll();
}, error => {
this.errorHandler.error(error);
});
}
getMetadataForAll() {
if (this.scanners && this.scanners.length > 0) {
this.scanners.forEach((scanner, index) => {
if (scanner.uuid ) {
this.scanners[index].loadingMetadata = true;
this.configScannerService.getScannerMetadata(scanner.uuid)
.pipe(finalize(() => this.scanners[index].loadingMetadata = false))
.subscribe(response => {
this.scanners[index].metadata = response;
}, error => {
this.scanners[index].metadata = null;
});
}
});
}
}
addNewScanner(): void {
this.newScannerDialog.open();

View File

@ -11,7 +11,7 @@
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
</div>
<clr-control-error *ngIf="!isNameValid">
{{nameTooltip | translate}}
<span id="name-error">{{nameTooltip | translate}}</span>
</clr-control-error>
</div>
</div>
@ -33,7 +33,7 @@
<span class="spinner spinner-inline" [hidden]="!checkEndpointOnGoing"></span>
</div>
<clr-control-error *ngIf="!isEndpointValid || showEndpointError">
{{endpointTooltip | translate}}
<span id="endpoint-error">{{endpointTooltip | translate}}</span>
</clr-control-error>
</div>
</div>
@ -73,7 +73,7 @@
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
</div>
<clr-control-error *ngIf="!isPasswordValid">
{{"SCANNER.PASSWORD_REQUIRED" | translate}}
<span id="pwd-error">{{"SCANNER.PASSWORD_REQUIRED" | translate}}</span>
</clr-control-error>
</div>
</div>

View File

@ -65,9 +65,9 @@ describe('NewScannerFormComponent', () => {
nameInput.blur();
nameInput.dispatchEvent(new Event('blur'));
setTimeout(() => {
let el = fixture.nativeElement.querySelector('clr-control-error');
let el = fixture.nativeElement.querySelector('#name-error');
expect(el).toBeFalsy();
}, 900);
}, 11000);
});
it('endpoint url should be valid', () => {
@ -79,9 +79,9 @@ describe('NewScannerFormComponent', () => {
urlInput.blur();
urlInput.dispatchEvent(new Event('blur'));
setTimeout(() => {
let el = fixture.nativeElement.querySelector('clr-control-error');
let el = fixture.nativeElement.querySelector('#endpoint-error');
expect(el).toBeFalsy();
}, 900);
}, 11000);
});
it('auth should be valid', () => {
@ -96,7 +96,7 @@ describe('NewScannerFormComponent', () => {
passwordInput.value = "12345";
usernameInput.dispatchEvent(new Event('input'));
passwordInput.dispatchEvent(new Event('input'));
let el = fixture.nativeElement.querySelector('clr-control-error');
let el = fixture.nativeElement.querySelector('#pwd-error');
expect(el).toBeFalsy();
});
});

View File

@ -1,3 +1,5 @@
import { ScannerMetadata } from "./scanner-metadata";
export class Scanner {
name?: string;
description?: string;
@ -13,7 +15,8 @@ export class Scanner {
update_time?: any;
vendor?: string;
version?: string;
health?: boolean;
metadata?: ScannerMetadata;
loadingMetadata?: boolean;
constructor() {
}
}

View File

@ -18,8 +18,13 @@
<span id="scanner-name" class="scanner-name">{{scanner?.name}}</span>
<button *ngIf="scanners && scanners.length > 0" id="edit-scanner" class="btn btn-primary btn-sm" (click)="open()">{{'SCANNER.EDIT' | translate}}</button>
<span *ngIf="scanner?.disabled" class="label label-warning ml-1">{{'SCANNER.DISABLED' | translate}}</span>
<span *ngIf="scanner?.health" class="label label-success ml-1">{{'SCANNER.HEALTHY' | translate}}</span>
<span *ngIf="!scanner?.health" class="label label-danger ml-1">{{'SCANNER.UNHEALTHY' | translate}}</span>
<span *ngIf="scanner?.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2"></span>
<ng-template #elseBlockLoading>
<span *ngIf="scanner?.metadata;else elseBlock" class="label label-success ml-1">{{'SCANNER.HEALTHY' | translate}}</span>
<ng-template #elseBlock>
<span class="label label-danger ml-1">{{'SCANNER.UNHEALTHY' | translate}}</span>
</ng-template>
</ng-template>
</div>
</div>
</div>
@ -33,29 +38,29 @@
</div>
</div>
</div>
<div class="clr-form-control" *ngIf="scanner?.scanner">
<div class="clr-form-control" *ngIf="scanner?.metadata?.scanner?.name">
<label class="clr-control-label">{{'SCANNER.ADAPTER' | translate}}</label>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input [ngModel]="scanner?.scanner" readonly class="clr-input width-240" type="text" id="scanner-scanner"
<input [ngModel]="scanner?.metadata?.scanner?.name" readonly class="clr-input width-240" type="text" id="scanner-scanner"
autocomplete="off">
</div>
</div>
</div>
<div class="clr-form-control" *ngIf="scanner?.vendor">
<div class="clr-form-control" *ngIf="scanner?.metadata?.scanner?.vendor">
<label class="clr-control-label">{{'SCANNER.VENDOR' | translate}}</label>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input [ngModel]="scanner?.vendor" readonly class="clr-input width-240" type="text" id="scanner-vendor"
<input [ngModel]="scanner?.metadata?.scanner?.vendor" readonly class="clr-input width-240" type="text" id="scanner-vendor"
autocomplete="off">
</div>
</div>
</div>
<div class="clr-form-control" *ngIf="scanner?.version">
<div class="clr-form-control" *ngIf="scanner?.metadata?.scanner?.version">
<label class="clr-control-label">{{'SCANNER.VERSION' | translate}}</label>
<div class="clr-control-container">
<div class="clr-input-wrapper">
<input [ngModel]="scanner?.version" readonly class="clr-input width-240" type="text" id="scanner-version"
<input [ngModel]="scanner?.metadata?.scanner?.version" readonly class="clr-input width-240" type="text" id="scanner-version"
autocomplete="off">
</div>
</div>
@ -76,8 +81,13 @@
<clr-dg-cell>{{scanner.name}}</clr-dg-cell>
<clr-dg-cell>{{scanner.url}}</clr-dg-cell>
<clr-dg-cell>
<span *ngIf="scanner.health" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
<span *ngIf="!scanner.health" class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
<span *ngIf="scanner.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2">Loading...</span>
<ng-template #elseBlockLoading>
<span *ngIf="scanner.metadata;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
<ng-template #elseBlock>
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
</ng-template>
</ng-template>
</clr-dg-cell>
<clr-dg-cell>
<span *ngIf="scanner.is_default" class="label label-info">{{scanner.is_default}}</span>

View File

@ -56,11 +56,24 @@ export class ScannerComponent implements OnInit {
.subscribe(response => {
if (response && "{}" !== JSON.stringify(response)) {
this.scanner = response;
this.getScannerMetadata();
}
}, error => {
this.errorHandler.error(error);
});
}
getScannerMetadata() {
if (this.scanner && this.scanner.uuid) {
this.scanner.loadingMetadata = true;
this.configScannerService.getScannerMetadata(this.scanner.uuid)
.pipe(finalize(() => this.scanner.loadingMetadata = false))
.subscribe(response => {
this.scanner.metadata = response;
}, error => {
this.scanner.metadata = null;
});
}
}
getScanners() {
this.loading = true;
this.configScannerService.getScanners()
@ -75,6 +88,22 @@ export class ScannerComponent implements OnInit {
this.errorHandler.error(error);
});
}
getMetadataForAll() {
if (this.scanners && this.scanners.length > 0) {
this.scanners.forEach((scanner, index) => {
if (scanner.uuid ) {
this.scanners[index].loadingMetadata = true;
this.configScannerService.getScannerMetadata(scanner.uuid)
.pipe(finalize(() => this.scanners[index].loadingMetadata = false))
.subscribe(response => {
this.scanners[index].metadata = response;
}, error => {
this.scanners[index].metadata = null;
});
}
});
}
}
close() {
this.opened = false;
this.selectedScanner = null;
@ -87,6 +116,7 @@ export class ScannerComponent implements OnInit {
this.selectedScanner = s;
}
});
this.getMetadataForAll();
}
get valid(): boolean {
return this.selectedScanner

View File

@ -622,6 +622,7 @@
"PULL_COMMAND": "Pull Command",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "My Repository",
"PUBLIC_REPOSITORY": "Public Repository",
"DELETION_TITLE_REPO": "Confirm Repository Deletion",
@ -636,7 +637,7 @@
"FILTER_FOR_REPOSITORIES": "Filter Repositories",
"TAG": "Tag",
"SIZE": "Size",
"VULNERABILITY": "Vulnerability",
"VULNERABILITY": "Vulnerabilities",
"BUILD_HISTORY": "Build History",
"SIGNED": "Signed",
"AUTHOR": "Author",
@ -1291,6 +1292,9 @@
"ENABLED": "Enabled",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DELETE_SUCCESS": "Successfully deleted"
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
}
}

View File

@ -623,6 +623,7 @@
"PULL_COMMAND": "Comando Pull",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Mi Repositorio",
"PUBLIC_REPOSITORY": "Repositorio Público",
"DELETION_TITLE_REPO": "Confirmar Eliminación de Repositorio",
@ -637,7 +638,7 @@
"FILTER_FOR_REPOSITORIES": "Filtrar Repositorios",
"TAG": "Etiqueta",
"SIZE": "Size",
"VULNERABILITY": "Vulnerability",
"VULNERABILITY": "Vulnerabilities",
"BUILD_HISTORY": "Construir Historia",
"SIGNED": "Firmada",
"AUTHOR": "Autor",
@ -1288,6 +1289,9 @@
"ENABLED": "Enabled",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DELETE_SUCCESS": "Successfully deleted"
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
}
}

View File

@ -612,6 +612,7 @@
"PULL_COMMAND": "Commande de Pull",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Mon Dépôt",
"PUBLIC_REPOSITORY": "Dépôt Public",
"DELETION_TITLE_REPO": "Confirmer la Suppresion du Dépôt",
@ -1260,6 +1261,9 @@
"ENABLED": "Enabled",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DELETE_SUCCESS": "Successfully deleted"
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
}
}

View File

@ -622,6 +622,7 @@
"PULL_COMMAND": "Comando de Pull",
"PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Meu Repositório",
"PUBLIC_REPOSITORY": "Repositório Público",
"DELETION_TITLE_REPO": "Confirmar remoção de repositório",
@ -1285,7 +1286,10 @@
"ENABLED": "Enabled",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DELETE_SUCCESS": "Successfully deleted"
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
}
}

View File

@ -621,6 +621,7 @@
"PULL_COMMAND": "İndirme Komutu",
"PULL_TIME": "İndirme Zamanı",
"PUSH_TIME": "Yükleme Zamanı",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Depom",
"PUBLIC_REPOSITORY": "Genel Depo",
"DELETION_TITLE_REPO": "Depo Silme İşlemini Onaylayın",
@ -1290,6 +1291,9 @@
"ENABLED": "Enabled",
"ENABLE": "Enable",
"DISABLE": "Disable",
"DELETE_SUCCESS": "Successfully deleted"
"DELETE_SUCCESS": "Successfully deleted",
"TOTAL": "Total",
"FIXABLE": "Fixable",
"DURATION": "Duration:"
}
}

View File

@ -623,6 +623,7 @@
"PULL_COMMAND": "Pull命令",
"PULL_TIME": "拉取时间",
"PUSH_TIME": "推送时间",
"IMMUTABLE": "保留的",
"MY_REPOSITORY": "我的仓库",
"PUBLIC_REPOSITORY": "公共仓库",
"DELETION_TITLE_REPO": "删除镜像仓库确认",
@ -1287,6 +1288,9 @@
"ENABLED": "启用",
"ENABLE": "启用",
"DISABLE": "禁用",
"DELETE_SUCCESS": "删除成功"
"DELETE_SUCCESS": "删除成功",
"TOTAL": "总计",
"FIXABLE": "可修复",
"DURATION": "扫描用时:"
}
}

View File

@ -73,7 +73,7 @@ class TestProjects(unittest.TestCase):
#5. Get project quota
quota = self.system.get_project_quota("project", TestProjects.project_test_quota_id, **ADMIN_CLIENT)
self.assertEqual(quota[0].used["count"], 1)
self.assertEqual(quota[0].used["storage"], 2791709)
self.assertEqual(quota[0].used["storage"], 2789174)
#6. Delete repository(RA) by user(UA);
self.repo.delete_repoitory(TestProjects.repo_name, **ADMIN_CLIENT)

View File

@ -54,6 +54,6 @@ Generate And Return Secret
Retry Element Click ${more_btn}
Retry Element Click ${generate_secret_btn}
Retry Double Keywords When Error Retry Element Click ${confirm_btn} Retry Wait Until Page Not Contains Element ${confirm_btn}
Retry Wait Until Page Contains generate CLI secret success
Retry Wait Until Page Contains Cli secret setting is successful
${secret}= Get Secrete By API ${url}
[Return] ${secret}

View File

@ -24,5 +24,5 @@ sudo curl -o /home/travis/gopath/src/github.com/goharbor/harbor/tests/apitests/p
sudo apt-get update && sudo apt-get install -y --no-install-recommends python-dev openjdk-7-jdk libssl-dev && sudo apt-get autoremove -y && sudo rm -rf /var/lib/apt/lists/*
sudo wget https://bootstrap.pypa.io/get-pip.py && sudo python ./get-pip.py && sudo pip install --ignore-installed urllib3 chardet requests && sudo pip install robotframework==3.0.4 robotframework-httplibrary requests dbbot robotframework-pabot --upgrade
sudo make swagger_client
sudo make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage CLARITYIMAGE=goharbor/harbor-clarity-ui-builder:1.6.0 NOTARYFLAG=true CLAIRFLAG=true CHARTFLAG=true
sudo make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage CLARITYIMAGE=goharbor/harbor-clarity-ui-builder:1.6.0 NOTARYFLAG=true CLAIRFLAG=true CHARTFLAG=true
sleep 10

View File

@ -2,5 +2,5 @@
set -e
sudo make package_online VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true CLAIRFLAG=true MIGRATORFLAG=false CHARTFLAG=true HTTPPROXY=
sudo make package_offline VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true CLAIRFLAG=true MIGRATORFLAG=false CHARTFLAG=true HTTPPROXY=
sudo make package_online VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true CLAIRFLAG=true MIGRATORFLAG=false CHARTFLAG=true HTTPPROXY=
sudo make package_offline VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true CLAIRFLAG=true MIGRATORFLAG=false CHARTFLAG=true HTTPPROXY=