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 sudo: true
language: go language: go
go: go:
- 1.12.5 - 1.12.12
go_import_path: github.com/goharbor/harbor go_import_path: github.com/goharbor/harbor
services: services:
- docker - docker
dist: trusty dist: trusty
matrix: matrix:
include: include:
- go: 1.12.5 - go: 1.12.12
env: env:
- UTTEST=true - UTTEST=true
- go: 1.12.5 - go: 1.12.12
env: env:
- APITEST_DB=true - APITEST_DB=true
- go: 1.12.5 - go: 1.12.12
env: env:
- APITEST_LDAP=true - APITEST_LDAP=true
- go: 1.12.5 - go: 1.12.12
env: env:
- OFFLINE=true - OFFLINE=true
- language: node_js - 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 **Allegis:** Harbor is used at Allegis as a secure private registry to store
and scan customized container images for different business applications, like and scan customized container images for different business applications, like
ELK stack, as part of their CI/CD pipeline. 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.6 | 1.9.2 |
| 1.7 | 1.9.2 | | 1.7 | 1.9.2 |
| 1.8 | 1.11.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. Ensure your GOPATH and PATH have been configured in accordance with the Go environment instructions.

View File

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

View File

@ -3,28 +3,6 @@ guidelines.
[GOVERNANCE.md](https://github.com/goharbor/community/blob/master/GOVERNANCE.md) [GOVERNANCE.md](https://github.com/goharbor/community/blob/master/GOVERNANCE.md)
describes governance guidelines and maintainer responsibilities. 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 [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.
| 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) |

View File

@ -11,17 +11,16 @@
|![notification](docs/img/bell-outline-badged.svg)Community Meeting| |![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> </br> </br>
**Note**: The `master` branch may be in an *unstable or even broken state* during development. **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"> <img alt="Harbor" src="docs/img/harbor_logo.png">

View File

@ -1,41 +1,12 @@
## Harbor Roadmap ## Harbor Roadmap
### About this document ### 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? ### 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? ### 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. 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.
---
### 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.

View File

@ -43,25 +43,25 @@ You can compile the code by one of the three approaches:
- Get official Golang image from docker hub: - Get official Golang image from docker hub:
```sh ```sh
$ docker pull golang:1.12.5 $ docker pull golang:1.12.12
``` ```
- Build, install and bring up Harbor without Notary: - Build, install and bring up Harbor without Notary:
```sh ```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: - Build, install and bring up Harbor with Notary:
```sh ```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: - Build, install and bring up Harbor with Clair:
```sh ```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 #### 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 \ 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 \ -key ca.key \
-out ca.crt -out ca.crt
``` ```
@ -36,9 +36,9 @@ If you use FQDN like **yourdomain.com** to connect your registry host, then you
``` ```
openssl req -sha512 -new \ 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 \ -key yourdomain.com.key \
-out yourdomain.com.csr -out yourdomain.com.csr
``` ```
**3) Generate the certificate of your registry host:** **3) Generate the certificate of your registry host:**
@ -52,7 +52,7 @@ cat > v3.ext <<-EOF
authorityKeyIdentifier=keyid,issuer authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth extendedKeyUsage = serverAuth
subjectAltName = @alt_names subjectAltName = @alt_names
[alt_names] [alt_names]
@ -75,17 +75,17 @@ EOF
**1) Configure Server Certificate and Key for Harbor** **1) Configure Server Certificate and Key for Harbor**
After obtaining the **yourdomain.com.crt** and **yourdomain.com.key** files, After obtaining the **yourdomain.com.crt** and **yourdomain.com.key** files,
you can put them into directory such as ```/root/cert/```: you can put them into directory such as ```/root/cert/```:
``` ```
cp yourdomain.com.crt /data/cert/ cp yourdomain.com.crt /data/cert/
cp yourdomain.com.key /data/cert/ cp yourdomain.com.key /data/cert/
``` ```
**2) Configure Server Certificate, Key and CA for Docker** **2) Configure Server Certificate, Key and CA for Docker**
The Docker daemon interprets ```.crt``` files as CA certificates and ```.cert``` files as client certificates. The Docker daemon interprets ```.crt``` files as CA certificates and ```.cert``` files as client certificates.
Convert server ```yourdomain.com.crt``` to ```yourdomain.com.cert```: Convert server ```yourdomain.com.crt``` to ```yourdomain.com.cert```:
@ -105,7 +105,7 @@ The following illustrates a configuration with custom certificates:
``` ```
/etc/docker/certs.d/ /etc/docker/certs.d/
└── yourdomain.com:port └── yourdomain.com:port
├── yourdomain.com.cert <-- Server certificate signed by CA ├── yourdomain.com.cert <-- Server certificate signed by CA
├── yourdomain.com.key <-- Server key signed by CA ├── yourdomain.com.key <-- Server key signed by CA
└── ca.crt <-- Certificate authority that signed the registry certificate └── ca.crt <-- Certificate authority that signed the registry certificate
@ -153,11 +153,11 @@ Finally, restart Harbor:
``` ```
After setting up HTTPS for Harbor, you can verify it by the following steps: After setting up HTTPS for Harbor, you can verify it by the following steps:
* Open a browser and enter the address: https://yourdomain.com. It should display the user interface of Harbor. * Open a browser and enter the address: https://yourdomain.com. It should display the user interface of Harbor.
* Notice that some browser may still shows the warning regarding Certificate Authority (CA) unknown for security reason even though we signed certificates by self-signed CA and deploy the CA to the place mentioned above. It is because self-signed CA essentially is not a trusted third-party CA. You can import the CA to the browser on your own to solve the warning. * Notice that some browser may still shows the warning regarding Certificate Authority (CA) unknown for security reason even though we signed certificates by self-signed CA and deploy the CA to the place mentioned above. It is because self-signed CA essentially is not a trusted third-party CA. You can import the CA to the browser on your own to solve the warning.
* On a machine with Docker daemon, make sure the option "-insecure-registry" for https://yourdomain.com is not present. * On a machine with Docker daemon, make sure the option "-insecure-registry" for https://yourdomain.com is not present.
* If you mapped nginx port 443 to another port, then you should instead create the directory ```/etc/docker/certs.d/yourdomain.com:port``` (or your registry host IP:port). Then run any docker command to verify the setup, e.g. * If you mapped nginx port 443 to another port, then you should instead create the directory ```/etc/docker/certs.d/yourdomain.com:port``` (or your registry host IP:port). Then run any docker command to verify the setup, e.g.
@ -173,21 +173,21 @@ If you've mapped nginx 443 port to another, you need to add the port to login, l
## Troubleshooting ## Troubleshooting
1. You may get an intermediate certificate from a certificate issuer. In this case, you should merge the intermediate certificate with your own certificate to create a certificate bundle. You can achieve this by the below command: 1. You may get an intermediate certificate from a certificate issuer. In this case, you should merge the intermediate certificate with your own certificate to create a certificate bundle. You can achieve this by the below command:
``` ```
cat intermediate-certificate.pem >> yourdomain.com.crt cat intermediate-certificate.pem >> yourdomain.com.crt
``` ```
2. On some systems where docker daemon runs, you may need to trust the certificate at OS level. 2. On some systems where docker daemon runs, you may need to trust the certificate at OS level.
On Ubuntu, this can be done by below commands: On Ubuntu, this can be done by below commands:
```sh ```sh
cp yourdomain.com.crt /usr/local/share/ca-certificates/yourdomain.com.crt cp yourdomain.com.crt /usr/local/share/ca-certificates/yourdomain.com.crt
update-ca-certificates update-ca-certificates
``` ```
On Red Hat (CentOS etc), the commands are: On Red Hat (CentOS etc), the commands are:
```sh ```sh
cp yourdomain.com.crt /etc/pki/ca-trust/source/anchors/yourdomain.com.crt cp yourdomain.com.crt /etc/pki/ca-trust/source/anchors/yourdomain.com.crt
update-ca-trust update-ca-trust

View File

@ -1,18 +1,32 @@
# Registry Landscape # 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. 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 | Table updated on 10/21/2019 against Harbor 1.9.
| -------------: | :----: | :---------------------: | :--: | :-------------------------------: | :-----------------: | :---------: |
| Local Auth | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | | Feature | Harbor | Docker Trusted Registry | Quay | Cloud Providers (GCP, AWS, Azure) | Docker Distribution | Artifactory | GitLab |
| LDAP-based Auth | ✓ | ✓ | ✓ | partial | ✗ | ✓ | | -------------: | :----: | :---------------------: | :-----: | :-------------------------------: | :-----------------: | :---------: | :------: |
| Content Trust and Validation | ✓ | ✓ | ✗ | ✗ | partial | partial | | Ability to Determine Version of Binaries in Containers | ✓ | ✓ | ✓ | ✗ | ✗ | ? | ? |
| Vulnerability Scanning & Monitoring | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | | Artifact Repository (rpms, git, jar, etc) | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | partial |
| Replication | ✓ | ✓ | ✓ | n/a | ✗ | ✓ | | Audit Logs | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
| Multi-Tenancy (projects, teams, etc.) | ✓ | ✓ | ✓ | partial | ✗ | ✓ | | Content Trust and Validation | ✓ | ✓ | ✗ | ✗ | partial | partial | ✗ |
| Role-Based Access Control | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | | Custom TLS Certificates | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
| Custom TLS Certificates | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | | Helm Chart Repository Manager | ✓ | ✗ | partial | ✗ | ✗ | ✓ | ✗ |
| Ability to Determine Version of Binaries in Containers | ✓ | ✓ | ✓ | ✗ | ✗ | ? | | LDAP-based Auth | ✓ | ✓ | ✓ | partial | ✗ | ✓ | ✓ |
| Upstream Registry Proxy Cache | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | | Local Auth | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
| Audit Logs | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | | 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. description: Forbidden.
'404': '404':
description: Repository not found. description: Repository not found.
'412':
description: Precondition Failed.
put: put:
summary: Update description of the repository. summary: Update description of the repository.
description: | description: |

View File

@ -36,10 +36,10 @@ version | set harbor version
#### EXAMPLE: #### EXAMPLE:
#### Build and run harbor from source code. #### 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 ### 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 ### Start harbor with notary
make -e NOTARYFLAG=true start make -e NOTARYFLAG=true start

View File

@ -97,7 +97,7 @@ DOCKERIMAGENAME_MIGRATOR=goharbor/harbor-migrator
# for chart server (chartmuseum) # for chart server (chartmuseum)
DOCKERFILEPATH_CHART_SERVER=$(DOCKERFILEPATH)/chartserver DOCKERFILEPATH_CHART_SERVER=$(DOCKERFILEPATH)/chartserver
DOCKERFILENAME_CHART_SERVER=Dockerfile 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_MAIN_PATH=cmd/chartmuseum
CHART_SERVER_BIN_NAME=chartm CHART_SERVER_BIN_NAME=chartm
@ -195,7 +195,7 @@ _build_registry:
rm -rf $(DOCKERFILEPATH_REG)/binary && mkdir -p $(DOCKERFILEPATH_REG)/binary && \ 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); \ $(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/registry/release-$(REGISTRYVERSION)/registry, $(DOCKERFILEPATH_REG)/binary/registry); \
else \ else \
cd $(DOCKERFILEPATH_REG) && $(DOCKERFILEPATH_REG)/builder $(REGISTRYVERSION) && cd - ; \ cd $(DOCKERFILEPATH_REG) && $(DOCKERFILEPATH_REG)/builder $(REGISTRY_SRC_TAG) && cd - ; \
fi fi
@echo "building registry container for photon..." @echo "building registry container for photon..."
@chmod 655 $(DOCKERFILEPATH_REG)/binary/registry && $(DOCKERBUILD) -f $(DOCKERFILEPATH_REG)/$(DOCKERFILENAME_REG) -t $(DOCKERIMAGENAME_REG):$(REGISTRYVERSION)-$(VERSIONTAG) . @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 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 #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/ ADD . /go/src/github.com/goharbor/harbor-scanner-clair/
WORKDIR /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 git clone https://github.com/danielpacak/harbor-scanner-clair.git $TEMP
cd $TEMP; git checkout $VERSION; cd - 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 cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t clair-adapter-golang $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/ ADD . /go/src/github.com/coreos/clair/
WORKDIR /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 git clone https://github.com/coreos/clair.git $TEMP
cd $TEMP; git checkout $VERSION; cd - 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 cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t clair-golang $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 NOTARY_VERSION
ARG MIGRATE_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:/go/bin/migrate binary/
docker cp $ID:/migrations 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 rm -f $ID
docker rmi -f notary-binary 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 DISTRIBUTION_DIR /go/src/github.com/docker/distribution
ENV BUILDTAGS include_oss include_gcs 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 git apply 2879.patch
cd $cur cd $cur
echo 'build the registry binary bases on the golang:1.11...' echo 'build the registry binary ...'
cp Dockerfile.binary $TEMP cp Dockerfile.binary $TEMP
docker build -f $TEMP/Dockerfile.binary -t registry-golang $TEMP docker build -f $TEMP/Dockerfile.binary -t registry-golang $TEMP

View File

@ -18,6 +18,7 @@ package metadata
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"strconv" "strconv"
"strings" "strings"
@ -139,12 +140,12 @@ type Int64Type struct {
} }
func (t *Int64Type) validate(str string) error { func (t *Int64Type) validate(str string) error {
_, err := strconv.ParseInt(str, 10, 64) _, err := parseInt64(str)
return err return err
} }
func (t *Int64Type) get(str string) (interface{}, error) { func (t *Int64Type) get(str string) (interface{}, error) {
return strconv.ParseInt(str, 10, 64) return parseInt64(str)
} }
// BoolType ... // BoolType ...
@ -194,7 +195,7 @@ type QuotaType struct {
} }
func (t *QuotaType) validate(str string) error { func (t *QuotaType) validate(str string) error {
val, err := strconv.ParseInt(str, 10, 64) val, err := parseInt64(str)
if err != nil { if err != nil {
return err return err
} }
@ -205,3 +206,18 @@ func (t *QuotaType) validate(str string) error {
return nil 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 package metadata
import ( import (
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestIntType_validate(t *testing.T) { func TestIntType_validate(t *testing.T) {
@ -96,3 +97,33 @@ func TestMapType_get(t *testing.T) {
result, _ := test.get(`{"sample":"abc", "another":"welcome"}`) result, _ := test.get(`{"sample":"abc", "another":"welcome"}`)
assert.Equal(t, map[string]interface{}{"sample": "abc", "another": "welcome"}, result) 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"` Author string `json:"author"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Config *TagCfg `json:"config"` Config *TagCfg `json:"config"`
Immutable bool `json:"immutable"`
} }
// TagCfg ... // TagCfg ...

View File

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

View File

@ -28,7 +28,7 @@ import (
"github.com/goharbor/harbor/src/jobservice/logger" "github.com/goharbor/harbor/src/jobservice/logger"
"github.com/goharbor/harbor/src/pkg/scan/api/scan" "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/schema1"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
@ -45,6 +45,8 @@ import (
"github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/config"
notifierEvt "github.com/goharbor/harbor/src/core/notifier/event" notifierEvt "github.com/goharbor/harbor/src/core/notifier/event"
coreutils "github.com/goharbor/harbor/src/core/utils" 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"
"github.com/goharbor/harbor/src/replication/event" "github.com/goharbor/harbor/src/replication/event"
"github.com/goharbor/harbor/src/replication/model" "github.com/goharbor/harbor/src/replication/model"
@ -283,11 +285,6 @@ func (ra *RepositoryAPI) Delete() {
} }
for _, t := range tags { 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 err = rc.DeleteTag(t); err != nil {
if regErr, ok := err.(*commonhttp.Error); ok { if regErr, ok := err.(*commonhttp.Error); ok {
if regErr.Code == http.StatusNotFound { if regErr.Code == http.StatusNotFound {
@ -298,6 +295,11 @@ func (ra *RepositoryAPI) Delete() {
return return
} }
log.Infof("delete tag: %s:%s", repoName, t) 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) { go func(tag string) {
e := &event.Event{ 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 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 // GetManifests returns the manifest of a tag
func (ra *RepositoryAPI) GetManifests() { func (ra *RepositoryAPI) GetManifests() {
repoName := ra.GetString(":splat") 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} var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
// MiddlewaresLocal ... // 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 ( import (
"fmt" "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/common/utils/log"
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
"github.com/goharbor/harbor/src/core/middlewares/util" "github.com/goharbor/harbor/src/core/middlewares/util"
"github.com/goharbor/harbor/src/pkg/art" middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"net/http" "net/http"
) )
type immutableHandler struct { type immutableHandler struct {
next http.Handler builders []interceptor.Builder
next http.Handler
} }
// New ... // 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{ return &immutableHandler{
next: next, builders: builders,
next: next,
} }
} }
// ServeHTTP ... // ServeHTTP ...
func (rh immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (rh *immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if match, _, _ := util.MatchPushManifest(req); !match {
interceptor, err := rh.getInterceptor(req)
if err != nil {
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
}
if interceptor == nil {
rh.next.ServeHTTP(rw, req) rh.next.ServeHTTP(rw, req)
return return
} }
info, ok := util.ManifestInfoFromContext(req.Context())
if !ok { if err := interceptor.HandleRequest(req); err != nil {
var err error log.Warningf("Error occurred when to handle request in immutable handler: %v", err)
info, err = util.ParseManifestInfoFromPath(req) if _, ok := err.(middlerware_err.ErrImmutable); ok {
if err != nil { http.Error(rw, util.MarshalError("DENIED",
log.Error(err) fmt.Sprintf("The tag is immutable, cannot be overwrite: %v", err)), http.StatusPreconditionFailed)
rh.next.ServeHTTP(rw, req)
return 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
}
} }
_, repoName := common_util.ParseRepository(info.Repository) return nil, nil
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 {
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
http.Error(rw, util.MarshalError("DENIED",
fmt.Sprintf("The tag:%s:%s is immutable, cannot be overwrite.", info.Repository, info.Tag)), http.StatusPreconditionFailed)
return
} }

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

View File

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

View File

@ -209,6 +209,10 @@
</clr-tooltip> </clr-tooltip>
</label> </label>
</div> </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> </div>
<div class="loading-center"> <div class="loading-center">

View File

@ -203,6 +203,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}) })
}), }),
filters: this.fb.array([]), filters: this.fb.array([]),
enabled: true,
deletion: false, deletion: false,
override: true override: true
}); });
@ -228,6 +229,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
} }
}, },
deletion: false, deletion: false,
enabled: true,
override: true override: true
}); });
this.isPushMode = true; this.isPushMode = true;
@ -251,6 +253,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
dest_registry: rule.dest_registry, dest_registry: rule.dest_registry,
trigger: rule.trigger, trigger: rule.trigger,
deletion: rule.deletion, deletion: rule.deletion,
enabled: rule.enabled,
override: rule.override override: rule.override
}); });
let filtersArray = this.getFilterArray(rule); 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 *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between jobsRow"> <div class="row flex-items-xs-between jobsRow">
<h5 class="flex-items-xs-bottom option-left-down">{{'REPLICATION.REPLICATION_EXECUTIONS' | translate}}</h5> <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="execution-select">
<div class="select filter-tag" [hidden]="!isOpenFilterTag"> <div class="select filter-tag" [hidden]="!isOpenFilterTag">
<select (change)="doFilterJob($event)"> <select (change)="doFilterJob($event)">

View File

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

View File

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

View File

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

View File

@ -73,6 +73,19 @@ export abstract class ScanningResultService {
* @memberOf ScanningResultService * @memberOf ScanningResultService
*/ */
abstract startScanningAll(): Observable<any>; 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() @Injectable()
@ -153,4 +166,14 @@ export class ScanningResultDefaultService extends ScanningResultService {
}) })
, catchError(error => observableThrowError(error))); , 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> </section>
</div> </div>
</div> </div>
<div class="col-md-4 col-sm-6"> <div class="col-md-4 col-sm-6 margin-top-5px">
<div class="vulnerability" [hidden]="hasCve"> <div class="vulnerability" [hidden]="hasCve || showStatBar">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="vulnerabilitySummary"></hbr-vulnerability-bar> <hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="vulnerabilitySummary"></hbr-vulnerability-bar>
</div> </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>
<div *ngIf="!withAdmiral && tagDetails?.labels?.length"> <div *ngIf="!withAdmiral && tagDetails?.labels?.length">
<div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div> <div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div>

View File

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

View File

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

View File

@ -93,6 +93,7 @@
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'> <clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-cell class="truncated flex-max-width"> <clr-dg-cell class="truncated flex-max-width">
<a href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a> <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>
<clr-dg-cell>{{sizeTransform(t.size)}}</clr-dg-cell> <clr-dg-cell>{{sizeTransform(t.size)}}</clr-dg-cell>
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"> <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 mockHasRetagImagePermission: boolean = true;
let mockHasDeleteImagePermission: boolean = true; let mockHasDeleteImagePermission: boolean = true;
let mockHasScanImagePermission: 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(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@ -141,6 +150,7 @@ describe("TagComponent (inline template)", () => {
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }, { provide: ScanningResultService, useClass: ScanningResultDefaultService },
{ provide: LabelService, useClass: LabelDefaultService }, { provide: LabelService, useClass: LabelDefaultService },
{ provide: UserPermissionService, useClass: UserPermissionDefaultService }, { provide: UserPermissionService, useClass: UserPermissionDefaultService },
{ provide: mockErrorHandler, useValue: ErrorHandler },
{ provide: OperationService }, { provide: OperationService },
] ]
}).compileComponents(); }).compileComponents();
@ -169,15 +179,10 @@ describe("TagComponent (inline template)", () => {
let http: HttpClient; let http: HttpClient;
http = fixture.debugElement.injector.get(HttpClient); http = fixture.debugElement.injector.get(HttpClient);
spyScanner = spyOn(http, "get").and.returnValue(of(scannerMock)); spyScanner = spyOn(http, "get").and.returnValue(of(scannerMock));
spyOn(userPermissionService, "getPermission") spyOn(userPermissionService, "hasProjectPermissions")
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE ) .withArgs(comp.projectId, permissions )
.and.returnValue(of(mockHasAddLabelImagePermission)) .and.returnValue(of([mockHasAddLabelImagePermission, mockHasRetagImagePermission,
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL ) mockHasDeleteImagePermission, mockHasScanImagePermission]));
.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));
labelService = fixture.debugElement.injector.get(LabelService); 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 { TranslateService } from "@ngx-translate/core";
import { Comparator, Label, State, Tag, TagClickEvent } from "../service/interface"; 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 { ErrorHandler } from "../error-handler/error-handler";
import { ChannelService } from "../channel/index"; import { ChannelService } from "../channel/index";
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../shared/shared.const"; 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 { OperationService } from "../operation/operation.service";
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component"; import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
import { errorHandler as errorHandFn } from "../shared/shared.utils"; import { errorHandler as errorHandFn } from "../shared/shared.utils";
import { HttpClient } from "@angular/common/http";
import { ClrLoadingState } from "@clr/angular"; import { ClrLoadingState } from "@clr/angular";
export interface LabelState { export interface LabelState {
@ -160,7 +164,7 @@ export class TagComponent implements OnInit, AfterViewInit {
private ref: ChangeDetectorRef, private ref: ChangeDetectorRef,
private operationService: OperationService, private operationService: OperationService,
private channel: ChannelService, private channel: ChannelService,
private http: HttpClient private scanningService: ScanningResultService
) { } ) { }
ngOnInit() { ngOnInit() {
@ -220,9 +224,6 @@ export class TagComponent implements OnInit, AfterViewInit {
} }
ngAfterViewInit() { ngAfterViewInit() {
if (!this.withAdmiral) {
this.getAllLabels();
}
} }
public get filterLabelPieceWidth() { public get filterLabelPieceWidth() {
@ -726,21 +727,24 @@ export class TagComponent implements OnInit, AfterViewInit {
return st !== VULNERABILITY_SCAN_STATUS.RUNNING; return st !== VULNERABILITY_SCAN_STATUS.RUNNING;
} }
getImagePermissionRule(projectId: number): void { getImagePermissionRule(projectId: number): void {
let hasAddLabelImagePermission = this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, const permissions = [
USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE); {resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
let hasRetagImagePermission = this.userPermissionService.getPermission(projectId, {resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL); {resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
let hasDeleteImagePermission = this.userPermissionService.getPermission(projectId, {resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE); ];
let hasScanImagePermission = this.userPermissionService.getPermission(projectId, this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => {
USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE); this.hasAddLabelImagePermission = results[0];
forkJoin(hasAddLabelImagePermission, hasRetagImagePermission, hasDeleteImagePermission, hasScanImagePermission) this.hasRetagImagePermission = results[1];
.subscribe(permissions => { this.hasDeleteImagePermission = results[2];
this.hasAddLabelImagePermission = permissions[0] as boolean; this.hasScanImagePermission = results[3];
this.hasRetagImagePermission = permissions[1] as boolean; // only has label permission
this.hasDeleteImagePermission = permissions[2] as boolean; if (this.hasAddLabelImagePermission) {
this.hasScanImagePermission = permissions[3] as boolean; if (!this.withAdmiral) {
}, error => this.errorHandler.error(error)); this.getAllLabels();
}
}
}, error => this.errorHandler.error(error));
} }
// Trigger scan // Trigger scan
scanNow(t: Tag[]): void { scanNow(t: Tag[]): void {
@ -759,19 +763,27 @@ export class TagComponent implements OnInit, AfterViewInit {
getProjectScanner(): void { getProjectScanner(): void {
this.hasEnabledScanner = false; this.hasEnabledScanner = false;
this.scanBtnState = ClrLoadingState.LOADING; this.scanBtnState = ClrLoadingState.LOADING;
this.http.get(`/api/projects/${this.projectId}/scanner`) this.scanningService.getProjectScanner(this.projectId)
.pipe(map(response => response as any))
.pipe(catchError(error => observableThrowError(error)))
.subscribe(response => { .subscribe(response => {
if (response && "{}" !== JSON.stringify(response) && !response.disable if (response && "{}" !== JSON.stringify(response) && !response.disabled
&& response.health) { && response.uuid) {
this.hasEnabledScanner = true; this.getScannerMetadata(response.uuid);
} else {
this.scanBtnState = ClrLoadingState.ERROR;
} }
this.scanBtnState = ClrLoadingState.SUCCESS;
}, error => { }, error => {
this.scanBtnState = ClrLoadingState.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;
});
}
handleScanOverview(scanOverview: any) { handleScanOverview(scanOverview: any) {
if (scanOverview) { if (scanOverview) {
return scanOverview[DEFAULT_SUPPORTED_MIME_TYPE]; return scanOverview[DEFAULT_SUPPORTED_MIME_TYPE];

View File

@ -127,6 +127,6 @@ export class HistogramChartComponent implements OnInit, AfterViewInit, DoCheck {
}); });
} }
this.max = count; 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(), end_time: new Date(),
summary: { summary: {
total: 124, total: 124,
fixable: 50,
summary: { summary: {
"High": 5, "High": 5,
"Low": 5 "Low": 5

View File

@ -10,7 +10,7 @@
<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">
<clr-datagrid [clrDgLoading]="loading"> <clr-datagrid [clrDgLoading]="loading">
<clr-dg-action-bar> <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-action-bar>
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column> <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> <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 { UserPermissionService } from "../service/permission.service";
import { USERSTATICPERMISSION } from "../service/permission-static"; import { USERSTATICPERMISSION } from "../service/permission-static";
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from '../utils'; 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({ @Component({
@ -22,10 +23,13 @@ export class ResultGridComponent implements OnInit {
scanningResults: VulnerabilityItem[] = []; scanningResults: VulnerabilityItem[] = [];
dataCache: VulnerabilityItem[] = []; dataCache: VulnerabilityItem[] = [];
loading: boolean = false; loading: boolean = false;
shouldShowLoading: boolean = true;
@Input() tagId: string; @Input() tagId: string;
@Input() repositoryId: string; @Input() repositoryId: string;
@Input() projectId: number; @Input() projectId: number;
hasScanImagePermission: boolean; hasScanImagePermission: boolean;
hasEnabledScanner: boolean = false;
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
constructor( constructor(
private scanningService: ScanningResultService, private scanningService: ScanningResultService,
private channel: ChannelService, private channel: ChannelService,
@ -39,11 +43,41 @@ export class ResultGridComponent implements OnInit {
this.channel.tagDetail$.subscribe(tag => { this.channel.tagDetail$.subscribe(tag => {
this.loadResults(this.repositoryId, this.tagId); 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 { loadResults(repositoryId: string, tagId: string): void {
this.loading = true; // only show loading for one time
if (this.shouldShowLoading) {
this.loading = true;
this.shouldShowLoading = false;
}
this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId) this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId)
.pipe(finalize(() => this.loading = false)) .pipe(finalize(() => this.loading = false))
.subscribe((results) => { .subscribe((results) => {

View File

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

View File

@ -208,6 +208,9 @@ hr {
.margin-left-5 { .margin-left-5 {
margin-left: 5px; margin-left: 5px;
} }
.margin-left-10 {
margin-left: 10px;
}
.width-30 { .width-30 {
width: 30px; width: 30px;
@ -220,3 +223,53 @@ hr {
.width-150 { .width-150 {
width: 150px; 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 { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../utils";
import { TranslateService } from "@ngx-translate/core"; 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({ @Component({
selector: 'hbr-result-tip-histogram', selector: 'hbr-result-tip-histogram',
templateUrl: './result-tip-histogram.component.html', templateUrl: './result-tip-histogram.component.html',
@ -52,7 +64,13 @@ export class ResultTipHistogramComponent implements OnInit {
this.vulnerabilitySummary.summary) { this.vulnerabilitySummary.summary) {
return this.vulnerabilitySummary.summary.total; 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; return 0;
} }
@ -72,7 +90,21 @@ export class ResultTipHistogramComponent implements OnInit {
return 0; 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 { get highCount(): number {
if (this.sevSummary) { if (this.sevSummary) {
return this.sevSummary[VULNERABILITY_SEVERITY.HIGH]; return this.sevSummary[VULNERABILITY_SEVERITY.HIGH];
@ -139,6 +171,29 @@ export class ResultTipHistogramComponent implements OnInit {
return this.total === 0; 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() { passMetadataToChart() {
return [ return [
{ {

View File

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

View File

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

View File

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

View File

@ -65,9 +65,12 @@
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{scanner.url}}</clr-dg-cell> <clr-dg-cell>{{scanner.url}}</clr-dg-cell>
<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 #elseBlock> <ng-template #elseBlockLoading>
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span> <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> </ng-template>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{!scanner.disabled}}</clr-dg-cell> <clr-dg-cell>{{!scanner.disabled}}</clr-dg-cell>

View File

@ -57,10 +57,27 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
.pipe(finalize(() => this.onGoing = false)) .pipe(finalize(() => this.onGoing = false))
.subscribe(response => { .subscribe(response => {
this.scanners = response; this.scanners = response;
this.getMetadataForAll();
}, error => { }, error => {
this.errorHandler.error(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 { addNewScanner(): void {
this.newScannerDialog.open(); this.newScannerDialog.open();

View File

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

View File

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

View File

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

View File

@ -18,8 +18,13 @@
<span id="scanner-name" class="scanner-name">{{scanner?.name}}</span> <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> <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?.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?.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2"></span>
<span *ngIf="!scanner?.health" class="label label-danger ml-1">{{'SCANNER.UNHEALTHY' | translate}}</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> </div>
</div> </div>
@ -33,29 +38,29 @@
</div> </div>
</div> </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> <label class="clr-control-label">{{'SCANNER.ADAPTER' | translate}}</label>
<div class="clr-control-container"> <div class="clr-control-container">
<div class="clr-input-wrapper"> <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"> autocomplete="off">
</div> </div>
</div> </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> <label class="clr-control-label">{{'SCANNER.VENDOR' | translate}}</label>
<div class="clr-control-container"> <div class="clr-control-container">
<div class="clr-input-wrapper"> <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"> autocomplete="off">
</div> </div>
</div> </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> <label class="clr-control-label">{{'SCANNER.VERSION' | translate}}</label>
<div class="clr-control-container"> <div class="clr-control-container">
<div class="clr-input-wrapper"> <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"> autocomplete="off">
</div> </div>
</div> </div>
@ -76,8 +81,13 @@
<clr-dg-cell>{{scanner.name}}</clr-dg-cell> <clr-dg-cell>{{scanner.name}}</clr-dg-cell>
<clr-dg-cell>{{scanner.url}}</clr-dg-cell> <clr-dg-cell>{{scanner.url}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<span *ngIf="scanner.health" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span> <span *ngIf="scanner.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2">Loading...</span>
<span *ngIf="!scanner.health" class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</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>
<clr-dg-cell> <clr-dg-cell>
<span *ngIf="scanner.is_default" class="label label-info">{{scanner.is_default}}</span> <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 => { .subscribe(response => {
if (response && "{}" !== JSON.stringify(response)) { if (response && "{}" !== JSON.stringify(response)) {
this.scanner = response; this.scanner = response;
this.getScannerMetadata();
} }
}, error => { }, error => {
this.errorHandler.error(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() { getScanners() {
this.loading = true; this.loading = true;
this.configScannerService.getScanners() this.configScannerService.getScanners()
@ -75,6 +88,22 @@ export class ScannerComponent implements OnInit {
this.errorHandler.error(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;
});
}
});
}
}
close() { close() {
this.opened = false; this.opened = false;
this.selectedScanner = null; this.selectedScanner = null;
@ -87,6 +116,7 @@ export class ScannerComponent implements OnInit {
this.selectedScanner = s; this.selectedScanner = s;
} }
}); });
this.getMetadataForAll();
} }
get valid(): boolean { get valid(): boolean {
return this.selectedScanner return this.selectedScanner

View File

@ -622,6 +622,7 @@
"PULL_COMMAND": "Pull Command", "PULL_COMMAND": "Pull Command",
"PULL_TIME": "Pull Time", "PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time", "PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "My Repository", "MY_REPOSITORY": "My Repository",
"PUBLIC_REPOSITORY": "Public Repository", "PUBLIC_REPOSITORY": "Public Repository",
"DELETION_TITLE_REPO": "Confirm Repository Deletion", "DELETION_TITLE_REPO": "Confirm Repository Deletion",
@ -636,7 +637,7 @@
"FILTER_FOR_REPOSITORIES": "Filter Repositories", "FILTER_FOR_REPOSITORIES": "Filter Repositories",
"TAG": "Tag", "TAG": "Tag",
"SIZE": "Size", "SIZE": "Size",
"VULNERABILITY": "Vulnerability", "VULNERABILITY": "Vulnerabilities",
"BUILD_HISTORY": "Build History", "BUILD_HISTORY": "Build History",
"SIGNED": "Signed", "SIGNED": "Signed",
"AUTHOR": "Author", "AUTHOR": "Author",
@ -1291,6 +1292,9 @@
"ENABLED": "Enabled", "ENABLED": "Enabled",
"ENABLE": "Enable", "ENABLE": "Enable",
"DISABLE": "Disable", "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_COMMAND": "Comando Pull",
"PULL_TIME": "Pull Time", "PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time", "PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Mi Repositorio", "MY_REPOSITORY": "Mi Repositorio",
"PUBLIC_REPOSITORY": "Repositorio Público", "PUBLIC_REPOSITORY": "Repositorio Público",
"DELETION_TITLE_REPO": "Confirmar Eliminación de Repositorio", "DELETION_TITLE_REPO": "Confirmar Eliminación de Repositorio",
@ -637,7 +638,7 @@
"FILTER_FOR_REPOSITORIES": "Filtrar Repositorios", "FILTER_FOR_REPOSITORIES": "Filtrar Repositorios",
"TAG": "Etiqueta", "TAG": "Etiqueta",
"SIZE": "Size", "SIZE": "Size",
"VULNERABILITY": "Vulnerability", "VULNERABILITY": "Vulnerabilities",
"BUILD_HISTORY": "Construir Historia", "BUILD_HISTORY": "Construir Historia",
"SIGNED": "Firmada", "SIGNED": "Firmada",
"AUTHOR": "Autor", "AUTHOR": "Autor",
@ -1288,6 +1289,9 @@
"ENABLED": "Enabled", "ENABLED": "Enabled",
"ENABLE": "Enable", "ENABLE": "Enable",
"DISABLE": "Disable", "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_COMMAND": "Commande de Pull",
"PULL_TIME": "Pull Time", "PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time", "PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Mon Dépôt", "MY_REPOSITORY": "Mon Dépôt",
"PUBLIC_REPOSITORY": "Dépôt Public", "PUBLIC_REPOSITORY": "Dépôt Public",
"DELETION_TITLE_REPO": "Confirmer la Suppresion du Dépôt", "DELETION_TITLE_REPO": "Confirmer la Suppresion du Dépôt",
@ -1260,6 +1261,9 @@
"ENABLED": "Enabled", "ENABLED": "Enabled",
"ENABLE": "Enable", "ENABLE": "Enable",
"DISABLE": "Disable", "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_COMMAND": "Comando de Pull",
"PULL_TIME": "Pull Time", "PULL_TIME": "Pull Time",
"PUSH_TIME": "Push Time", "PUSH_TIME": "Push Time",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Meu Repositório", "MY_REPOSITORY": "Meu Repositório",
"PUBLIC_REPOSITORY": "Repositório Público", "PUBLIC_REPOSITORY": "Repositório Público",
"DELETION_TITLE_REPO": "Confirmar remoção de repositório", "DELETION_TITLE_REPO": "Confirmar remoção de repositório",
@ -1285,7 +1286,10 @@
"ENABLED": "Enabled", "ENABLED": "Enabled",
"ENABLE": "Enable", "ENABLE": "Enable",
"DISABLE": "Disable", "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_COMMAND": "İndirme Komutu",
"PULL_TIME": "İndirme Zamanı", "PULL_TIME": "İndirme Zamanı",
"PUSH_TIME": "Yükleme Zamanı", "PUSH_TIME": "Yükleme Zamanı",
"IMMUTABLE": "Immutable",
"MY_REPOSITORY": "Depom", "MY_REPOSITORY": "Depom",
"PUBLIC_REPOSITORY": "Genel Depo", "PUBLIC_REPOSITORY": "Genel Depo",
"DELETION_TITLE_REPO": "Depo Silme İşlemini Onaylayın", "DELETION_TITLE_REPO": "Depo Silme İşlemini Onaylayın",
@ -1290,6 +1291,9 @@
"ENABLED": "Enabled", "ENABLED": "Enabled",
"ENABLE": "Enable", "ENABLE": "Enable",
"DISABLE": "Disable", "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_COMMAND": "Pull命令",
"PULL_TIME": "拉取时间", "PULL_TIME": "拉取时间",
"PUSH_TIME": "推送时间", "PUSH_TIME": "推送时间",
"IMMUTABLE": "保留的",
"MY_REPOSITORY": "我的仓库", "MY_REPOSITORY": "我的仓库",
"PUBLIC_REPOSITORY": "公共仓库", "PUBLIC_REPOSITORY": "公共仓库",
"DELETION_TITLE_REPO": "删除镜像仓库确认", "DELETION_TITLE_REPO": "删除镜像仓库确认",
@ -1287,6 +1288,9 @@
"ENABLED": "启用", "ENABLED": "启用",
"ENABLE": "启用", "ENABLE": "启用",
"DISABLE": "禁用", "DISABLE": "禁用",
"DELETE_SUCCESS": "删除成功" "DELETE_SUCCESS": "删除成功",
"TOTAL": "总计",
"FIXABLE": "可修复",
"DURATION": "扫描用时:"
} }
} }

View File

@ -73,7 +73,7 @@ class TestProjects(unittest.TestCase):
#5. Get project quota #5. Get project quota
quota = self.system.get_project_quota("project", TestProjects.project_test_quota_id, **ADMIN_CLIENT) 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["count"], 1)
self.assertEqual(quota[0].used["storage"], 2791709) self.assertEqual(quota[0].used["storage"], 2789174)
#6. Delete repository(RA) by user(UA); #6. Delete repository(RA) by user(UA);
self.repo.delete_repoitory(TestProjects.repo_name, **ADMIN_CLIENT) 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 ${more_btn}
Retry Element Click ${generate_secret_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 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} ${secret}= Get Secrete By API ${url}
[Return] ${secret} [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 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 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 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 sleep 10

View File

@ -2,5 +2,5 @@
set -e 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_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.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.12 COMPILETAG=compile_golangimage NOTARYFLAG=true CLAIRFLAG=true MIGRATORFLAG=false CHARTFLAG=true HTTPPROXY=