mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-23 00:57:44 +01:00
Merge branch 'master' into https-install
This commit is contained in:
commit
b77fd20865
10
.travis.yml
10
.travis.yml
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
7
Makefile
7
Makefile
@ -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
|
||||||
|
26
OWNERS.md
26
OWNERS.md
@ -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) |
|
|
||||||
|
@ -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">
|
||||||
|
|
||||||
|
53
ROADMAP.md
53
ROADMAP.md
@ -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.
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,18 +1,32 @@
|
|||||||
# Registry Landscape
|
# Registry Landscape
|
||||||
The cloud native ecosystem is moving rapidly–registries 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 rapidly–registries 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 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||||
|
@ -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: |
|
||||||
|
@ -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
|
||||||
|
@ -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) .
|
||||||
|
@ -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
|
||||||
|
@ -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/
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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/
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 ...
|
||||||
|
@ -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},
|
||||||
},
|
},
|
||||||
|
@ -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")
|
||||||
|
@ -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}
|
||||||
|
54
src/core/middlewares/immutable/builder.go
Normal file
54
src/core/middlewares/immutable/builder.go
Normal 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
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
67
src/core/middlewares/interceptor/immutable/deletemf.go
Normal file
67
src/core/middlewares/interceptor/immutable/deletemf.go
Normal 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) {
|
||||||
|
}
|
65
src/core/middlewares/interceptor/immutable/pushmf.go
Normal file
65
src/core/middlewares/interceptor/immutable/pushmf.go
Normal 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) {
|
||||||
|
}
|
20
src/core/middlewares/util/error/immutable.go
Normal file
20
src/core/middlewares/util/error/immutable.go
Normal 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}
|
||||||
|
}
|
@ -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},
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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);
|
||||||
|
@ -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)">
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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}}">
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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];
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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> {{'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> {{'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>
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
||||||
|
@ -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%;
|
||||||
|
}
|
||||||
|
@ -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 [
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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": "扫描用时:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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}
|
@ -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
|
||||||
|
@ -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=
|
||||||
|
Loading…
Reference in New Issue
Block a user