mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-22 16:48:30 +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
|
||||
language: go
|
||||
go:
|
||||
- 1.12.5
|
||||
- 1.12.12
|
||||
go_import_path: github.com/goharbor/harbor
|
||||
services:
|
||||
- docker
|
||||
dist: trusty
|
||||
matrix:
|
||||
include:
|
||||
- go: 1.12.5
|
||||
- go: 1.12.12
|
||||
env:
|
||||
- UTTEST=true
|
||||
- go: 1.12.5
|
||||
- go: 1.12.12
|
||||
env:
|
||||
- APITEST_DB=true
|
||||
- go: 1.12.5
|
||||
- go: 1.12.12
|
||||
env:
|
||||
- APITEST_LDAP=true
|
||||
- go: 1.12.5
|
||||
- go: 1.12.12
|
||||
env:
|
||||
- OFFLINE=true
|
||||
- language: node_js
|
||||
|
@ -80,3 +80,6 @@ feature within Harbor before deploying images into production.
|
||||
**Allegis:** Harbor is used at Allegis as a secure private registry to store
|
||||
and scan customized container images for different business applications, like
|
||||
ELK stack, as part of their CI/CD pipeline.
|
||||
|
||||
# Adding a logo
|
||||
If you would like to add your logo to the `Users and Partners of Harbor` section of the website, add a PNG version of your logo to the docs/img directory in this repo and submit a pull request with your change. Name the image file something that reflects your company (e.g., if your company is called Acme, name the image acme.png). We will follow up and make the change in the goharbor.io website as well.
|
||||
|
@ -127,6 +127,7 @@ Harbor backend is written in [Go](http://golang.org/). If you don't have a Harbo
|
||||
| 1.6 | 1.9.2 |
|
||||
| 1.7 | 1.9.2 |
|
||||
| 1.8 | 1.11.2 |
|
||||
| 1.9 | 1.12.12 |
|
||||
|
||||
Ensure your GOPATH and PATH have been configured in accordance with the Go environment instructions.
|
||||
|
||||
|
7
Makefile
7
Makefile
@ -9,7 +9,7 @@
|
||||
# compile_golangimage:
|
||||
# compile from golang image
|
||||
# for example: make compile_golangimage -e GOBUILDIMAGE= \
|
||||
# golang:1.11.2
|
||||
# golang:1.12.12
|
||||
# compile_core, compile_jobservice: compile specific binary
|
||||
#
|
||||
# build: build Harbor docker images from photon baseimage
|
||||
@ -111,6 +111,9 @@ CLAIRADAPTERVERSION=c7db8b15
|
||||
# version of chartmuseum
|
||||
CHARTMUSEUMVERSION=v0.9.0
|
||||
|
||||
# version of registry for pulling the source code
|
||||
REGISTRY_SRC_TAG=v2.7.1
|
||||
|
||||
define VERSIONS_FOR_PREPARE
|
||||
VERSION_TAG: $(VERSIONTAG)
|
||||
REGISTRY_VERSION: $(REGISTRYVERSION)
|
||||
@ -138,7 +141,7 @@ GOINSTALL=$(GOCMD) install
|
||||
GOTEST=$(GOCMD) test
|
||||
GODEP=$(GOTEST) -i
|
||||
GOFMT=gofmt -w
|
||||
GOBUILDIMAGE=golang:1.12.5
|
||||
GOBUILDIMAGE=golang:1.12.12
|
||||
GOBUILDPATH=/harbor
|
||||
GOIMAGEBUILDCMD=/usr/local/go/bin/go
|
||||
GOIMAGEBUILD=$(GOIMAGEBUILDCMD) build -mod vendor
|
||||
|
26
OWNERS.md
26
OWNERS.md
@ -3,28 +3,6 @@ guidelines.
|
||||
[GOVERNANCE.md](https://github.com/goharbor/community/blob/master/GOVERNANCE.md)
|
||||
describes governance guidelines and maintainer responsibilities.
|
||||
|
||||
Maintainer list here is a copy of [MAINTAINERS.md](https://github.com/goharbor/community/blob/master/MAINTAINERS.md).
|
||||
[MAINTAINERS.md](https://github.com/goharbor/community/blob/master/MAINTAINERS.md) contains the project maintainers.
|
||||
|
||||
## Core Maintainers
|
||||
|
||||
| Core Maintainer | GitHub ID | Affiliation |
|
||||
| --------------- | --------- | ----------- |
|
||||
| Daniel Jiang | [reasonerjt](https://github.com/reasonerjt) | [VMware](https://www.github.com/vmware/) |
|
||||
| Steven Ren | [renmaosheng](https://github.com/renmaosheng) | [VMware](https://www.github.com/vmware/) |
|
||||
| Steven Zou | [steven-zou](https://github.com/steven-zou) | [VMware](https://www.github.com/vmware/) |
|
||||
| Henry Zhang| [hainingzhang](https://github.com/hainingzhang)| [VMware](https://www.github.com/vmware/) |
|
||||
| Michael Michael |[michmike](https://github.com/michmike)| [VMware](https://www.github.com/vmware/) |
|
||||
|
||||
## Maintainers
|
||||
|
||||
| Maintainer | GitHub ID | Affiliation |
|
||||
| ---------- | --------- | ----------- |
|
||||
| Daojun Zhang | [stonezdj](https://github.com/stonezdj) | [VMware](https://www.github.com/vmware/) |
|
||||
| Wenkai Yin | [ywk253100](https://github.com/ywk253100) | [VMware](https://www.github.com/vmware/) |
|
||||
| Yan Wang | [wy65701436](https://github.com/wy65701436) | [VMware](https://www.github.com/vmware/) |
|
||||
| Qian Deng | [ninjadq](https://github.com/ninjadq) | [VMware](https://www.github.com/vmware/) |
|
||||
| Mia Zhou | [zhoumeina](https://github.com/zhoumeina) | [VMware](https://www.github.com/vmware/) |
|
||||
| Nathan Lowe | [nlowe](https://github.com/nlowe) | [Hyland Software](https://github.com/HylandSoftware) |
|
||||
| De Chen | [cd1989](https://github.com/cd1989) | [Caicloud](https://github.com/caicloud) |
|
||||
| Mingming Pei | [mmpei](https://github.com/mmpei) | [Netease](https://github.com/netease) |
|
||||
| Fanjian Kong | [kofj](https://github.com/kofj) | [Qihoo360](https://github.com/Qihoo360) |
|
||||
[GUIDING_PRINCIPLES.md](https://github.com/goharbor/community/blob/master/GUIDING_PRINCIPLES.md) contains the project vision, values and principles and how we apply them in making decisions.
|
||||
|
@ -11,17 +11,16 @@
|
||||
|
||||
|![notification](docs/img/bell-outline-badged.svg)Community Meeting|
|
||||
|------------------|
|
||||
|The Harbor Project holds bi-weekly community calls, to join them and watch previous meeting notes and recordings, please see [meeting schedule](https://github.com/goharbor/community/blob/master/MEETING_SCHEDULE.md).|
|
||||
|The Harbor Project holds bi-weekly community calls in two different timezones. To join the community calls or to watch previous meeting notes and recordings, please visit the [meeting schedule](https://github.com/goharbor/community/blob/master/MEETING_SCHEDULE.md).|
|
||||
|
||||
Welcome to join below Harbor community events and meet with project maintainers and users:
|
||||
We welcome you to join the below Harbor community events and meet with project maintainers and users:
|
||||
|
||||
**May 20-24, 2019**, [KubeCon EU, Barcelona](https://events.linuxfoundation.org/events/kubecon-cloudnativecon-europe-2019/): Harbor Community Reception, Intro and Deep-dive sessions.
|
||||
**November 18-21, 2019**, [KubeCon US, San Diego](https://events19.linuxfoundation.org/events/kubecon-cloudnativecon-north-america-2019): Harbor Lunch & Learn led by Joe Beda, Intro and Deep-dive sessions.
|
||||
|
||||
**June 24-26, 2019**, [KubeCon Shanghai](https://www.lfasiallc.com/events/kubecon-cloudnativecon-china-2019/): Harbor community meetup, Harbor session.
|
||||
</br> </br>
|
||||
|
||||
**Note**: The `master` branch may be in an *unstable or even broken state* during development.
|
||||
Please use [releases](https://github.com/vmware/harbor/releases) instead of the `master` branch in order to get stable binaries.
|
||||
Please use [releases](https://github.com/vmware/harbor/releases) instead of the `master` branch in order to get a stable set of binaries.
|
||||
|
||||
<img alt="Harbor" src="docs/img/harbor_logo.png">
|
||||
|
||||
|
53
ROADMAP.md
53
ROADMAP.md
@ -1,41 +1,12 @@
|
||||
## Harbor Roadmap
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### How to add an item to the roadmap?
|
||||
Please open an issue to track any initiative on the roadmap of Harbor. We will work with and rely on our community to focus our efforts to improve Harbor.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 1. Notary
|
||||
The notary feature allows publishers to sign their images offline and to push the signed content to a notary server. This ensures the authenticity of images.
|
||||
|
||||
### 2. Vulnerability Scanning
|
||||
The capability to scan images for vulnerability.
|
||||
|
||||
### 3. Image replication enhancement
|
||||
To provide more sophisticated rule for image replication.
|
||||
- Image filtering by tags
|
||||
- Replication can be scheduled at a certain time using a rule like: one time only, daily, weekly, etc.
|
||||
- Image deletion can have the option not to be replicated to a remote instance.
|
||||
- Global replication rule: Instead of setting the rule of individual project, system admin can set a global rule for all projects.
|
||||
- Project admin can set replication policy of the project.
|
||||
|
||||
### 4. Authentication (OAuth2)
|
||||
In addition to LDAP/AD and local users, OAuth 2.0 can be used to authenticate a user.
|
||||
|
||||
### 5. High Availability
|
||||
Support multi-node deployment of Harbor for high availability, scalability and load-balancing purposes.
|
||||
|
||||
### 6. Statistics and description for repositories
|
||||
User can add a description to a repository. The access count of a repo can be aggregated and displayed.
|
||||
|
||||
### 7. Migration tool to move from an existing registry to Harbor
|
||||
A tool to migrate images from a vanilla registry server to Harbor, without the need to export/import a large amount of data.
|
||||
## Harbor Roadmap
|
||||
|
||||
### About this document
|
||||
|
||||
This document provides a link to the [Harbor Project board](https://github.com/orgs/goharbor/projects/1) that serves as the up to date description of items that are in the Harbor release pipeline. The board has separate swim lanes for each release. Most items are gathered from the community or include a feedback loop with the community. This should serve as a reference point for Harbor users and contributors to understand where the project is heading, and help determine if a contribution could be conflicting with a longer term plan.
|
||||
|
||||
### How to help?
|
||||
|
||||
Discussion on the roadmap can take place in threads under [Issues](https://github.com/goharbor/harbor/issues) or in [community meetings](https://github.com/goharbor/community/blob/master/MEETING_SCHEDULE.md). Please open and comment on an issue if you want to provide suggestions and feedback to an item in the roadmap. Please review the roadmap to avoid potential duplicated effort.
|
||||
|
||||
### How to add an item to the roadmap?
|
||||
Please open an issue to track any initiative on the roadmap of Harbor (Usually driven by new feature requests). We will work with and rely on our community to focus our efforts to improve Harbor.
|
||||
|
@ -43,25 +43,25 @@ You can compile the code by one of the three approaches:
|
||||
- Get official Golang image from docker hub:
|
||||
|
||||
```sh
|
||||
$ docker pull golang:1.12.5
|
||||
$ docker pull golang:1.12.12
|
||||
```
|
||||
|
||||
- Build, install and bring up Harbor without Notary:
|
||||
|
||||
```sh
|
||||
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage
|
||||
$ make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage
|
||||
```
|
||||
|
||||
- Build, install and bring up Harbor with Notary:
|
||||
|
||||
```sh
|
||||
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
$ make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
```
|
||||
|
||||
- Build, install and bring up Harbor with Clair:
|
||||
|
||||
```sh
|
||||
$ make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage CLAIRFLAG=true
|
||||
$ make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage CLAIRFLAG=true
|
||||
```
|
||||
|
||||
#### II. Compile code with your own Golang environment, then build Harbor
|
||||
|
@ -13,7 +13,7 @@ You can use certificates that are signed by a trusted third-party CA, or you ca
|
||||
```
|
||||
```
|
||||
openssl req -x509 -new -nodes -sha512 -days 3650 \
|
||||
-subj "/C=TW/ST=Taipei/L=Taipei/O=example/OU=Personal/CN=yourdomain.com" \
|
||||
-subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=yourdomain.com" \
|
||||
-key ca.key \
|
||||
-out ca.crt
|
||||
```
|
||||
@ -36,9 +36,9 @@ If you use FQDN like **yourdomain.com** to connect your registry host, then you
|
||||
|
||||
```
|
||||
openssl req -sha512 -new \
|
||||
-subj "/C=TW/ST=Taipei/L=Taipei/O=example/OU=Personal/CN=yourdomain.com" \
|
||||
-subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=yourdomain.com" \
|
||||
-key yourdomain.com.key \
|
||||
-out yourdomain.com.csr
|
||||
-out yourdomain.com.csr
|
||||
```
|
||||
|
||||
**3) Generate the certificate of your registry host:**
|
||||
@ -52,7 +52,7 @@ cat > v3.ext <<-EOF
|
||||
authorityKeyIdentifier=keyid,issuer
|
||||
basicConstraints=CA:FALSE
|
||||
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
@ -75,17 +75,17 @@ EOF
|
||||
|
||||
**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/```:
|
||||
|
||||
```
|
||||
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**
|
||||
|
||||
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```:
|
||||
|
||||
@ -105,7 +105,7 @@ The following illustrates a configuration with custom certificates:
|
||||
|
||||
```
|
||||
/etc/docker/certs.d/
|
||||
└── yourdomain.com:port
|
||||
└── yourdomain.com:port
|
||||
├── yourdomain.com.cert <-- Server certificate signed by CA
|
||||
├── yourdomain.com.key <-- Server key signed by CA
|
||||
└── 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:
|
||||
|
||||
* 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.
|
||||
|
||||
* 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.
|
||||
|
||||
@ -173,21 +173,21 @@ If you've mapped nginx 443 port to another, you need to add the port to login, l
|
||||
|
||||
|
||||
## 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.
|
||||
On Ubuntu, this can be done by below commands:
|
||||
|
||||
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:
|
||||
|
||||
```sh
|
||||
cp yourdomain.com.crt /usr/local/share/ca-certificates/yourdomain.com.crt
|
||||
update-ca-certificates
|
||||
```
|
||||
|
||||
On Red Hat (CentOS etc), the commands are:
|
||||
|
||||
```
|
||||
|
||||
On Red Hat (CentOS etc), the commands are:
|
||||
|
||||
```sh
|
||||
cp yourdomain.com.crt /etc/pki/ca-trust/source/anchors/yourdomain.com.crt
|
||||
update-ca-trust
|
||||
|
@ -1,18 +1,32 @@
|
||||
# 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.
|
||||
|
||||
| Feature | Harbor | Docker Trusted Registry | Quay | Cloud Providers (GCP, AWS, Azure) | Docker Distribution | Artifactory |
|
||||
| -------------: | :----: | :---------------------: | :--: | :-------------------------------: | :-----------------: | :---------: |
|
||||
| Local Auth | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
|
||||
| LDAP-based Auth | ✓ | ✓ | ✓ | partial | ✗ | ✓ |
|
||||
| Content Trust and Validation | ✓ | ✓ | ✗ | ✗ | partial | partial |
|
||||
| Vulnerability Scanning & Monitoring | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |
|
||||
| Replication | ✓ | ✓ | ✓ | n/a | ✗ | ✓ |
|
||||
| Multi-Tenancy (projects, teams, etc.) | ✓ | ✓ | ✓ | partial | ✗ | ✓ |
|
||||
| Role-Based Access Control | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
|
||||
| Custom TLS Certificates | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
|
||||
| Ability to Determine Version of Binaries in Containers | ✓ | ✓ | ✓ | ✗ | ✗ | ? |
|
||||
| Upstream Registry Proxy Cache | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ |
|
||||
| Audit Logs | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ |
|
||||
Table updated on 10/21/2019 against Harbor 1.9.
|
||||
|
||||
| Feature | Harbor | Docker Trusted Registry | Quay | Cloud Providers (GCP, AWS, Azure) | Docker Distribution | Artifactory | GitLab |
|
||||
| -------------: | :----: | :---------------------: | :-----: | :-------------------------------: | :-----------------: | :---------: | :------: |
|
||||
| Ability to Determine Version of Binaries in Containers | ✓ | ✓ | ✓ | ✗ | ✗ | ? | ? |
|
||||
| Artifact Repository (rpms, git, jar, etc) | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | partial |
|
||||
| Audit Logs | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
|
||||
| Content Trust and Validation | ✓ | ✓ | ✗ | ✗ | partial | partial | ✗ |
|
||||
| Custom TLS Certificates | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ |
|
||||
| Helm Chart Repository Manager | ✓ | ✗ | partial | ✗ | ✗ | ✓ | ✗ |
|
||||
| LDAP-based Auth | ✓ | ✓ | ✓ | partial | ✗ | ✓ | ✓ |
|
||||
| Local Auth | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ |
|
||||
| Multi-Tenancy (projects, teams, namespaces, etc) | ✓ | ✓ | ✓ | partial | ✗ | ✓ | ✓ |
|
||||
| Open Source | ✓ | partial | ✗ | ✗ | ✓ | partial | partial |
|
||||
| Project Quotas (by image count & storage consumption) | ✓ | ✗ | ✗ | partial | ✗ | ✗ | ✗ |
|
||||
| Replication between instances | ✓ | ✓ | ✓ | n/a | ✗ | ✓ | ✗ |
|
||||
| Replication between non-instances | ✓ | ✗ | ✓ | n/a | ✗ | ✗ | ✗ |
|
||||
| Robot Accounts for Helm Charts | ✓ | ✗ | ✗ | ? | ✗ | ✗ | ✗ |
|
||||
| Robot Accounts for Images | ✓ | ? | ✓ | ? | ✗ | ? | ? |
|
||||
| Role-Based Access Control | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✗ |
|
||||
| Single Sign On (OIDC) | ✓ | ✓ | ✓ | ✓ | ✗ | partial | ✗ |
|
||||
| Tag Retention Policy | ✓ | ✗ | partial | ✗ | ✗ | ✗ | ✗ |
|
||||
| Upstream Registry Proxy Cache | ✗ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ |
|
||||
| Vulnerability Scanning & Monitoring | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | partial |
|
||||
| Vulnerability Scanning Plugin Framework | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
||||
| Vulnerability Whitelisting | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
||||
| Webhooks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
@ -1089,6 +1089,8 @@ paths:
|
||||
description: Forbidden.
|
||||
'404':
|
||||
description: Repository not found.
|
||||
'412':
|
||||
description: Precondition Failed.
|
||||
put:
|
||||
summary: Update description of the repository.
|
||||
description: |
|
||||
|
@ -36,10 +36,10 @@ version | set harbor version
|
||||
#### EXAMPLE:
|
||||
|
||||
#### Build and run harbor from source code.
|
||||
make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
|
||||
### Package offline installer
|
||||
make package_offline GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
make package_offline GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true
|
||||
|
||||
### Start harbor with notary
|
||||
make -e NOTARYFLAG=true start
|
||||
|
@ -97,7 +97,7 @@ DOCKERIMAGENAME_MIGRATOR=goharbor/harbor-migrator
|
||||
# for chart server (chartmuseum)
|
||||
DOCKERFILEPATH_CHART_SERVER=$(DOCKERFILEPATH)/chartserver
|
||||
DOCKERFILENAME_CHART_SERVER=Dockerfile
|
||||
CHART_SERVER_CODE_BASE=github.com/helm/chartmuseum
|
||||
CHART_SERVER_CODE_BASE=https://github.com/helm/chartmuseum.git
|
||||
CHART_SERVER_MAIN_PATH=cmd/chartmuseum
|
||||
CHART_SERVER_BIN_NAME=chartm
|
||||
|
||||
@ -195,7 +195,7 @@ _build_registry:
|
||||
rm -rf $(DOCKERFILEPATH_REG)/binary && mkdir -p $(DOCKERFILEPATH_REG)/binary && \
|
||||
$(call _get_binary, https://storage.googleapis.com/harbor-builds/bin/registry/release-$(REGISTRYVERSION)/registry, $(DOCKERFILEPATH_REG)/binary/registry); \
|
||||
else \
|
||||
cd $(DOCKERFILEPATH_REG) && $(DOCKERFILEPATH_REG)/builder $(REGISTRYVERSION) && cd - ; \
|
||||
cd $(DOCKERFILEPATH_REG) && $(DOCKERFILEPATH_REG)/builder $(REGISTRY_SRC_TAG) && cd - ; \
|
||||
fi
|
||||
@echo "building registry container for photon..."
|
||||
@chmod 655 $(DOCKERFILEPATH_REG)/binary/registry && $(DOCKERBUILD) -f $(DOCKERFILEPATH_REG)/$(DOCKERFILENAME_REG) -t $(DOCKERIMAGENAME_REG):$(REGISTRYVERSION)-$(VERSIONTAG) .
|
||||
|
@ -30,4 +30,4 @@ cp compile.sh binary/
|
||||
docker run -it --rm -v $cur/binary:/go/bin --name golang_code_builder $GOLANG_IMAGE /bin/bash /go/bin/compile.sh $GIT_PATH $CODE_VERSION $MAIN_GO_PATH $BIN_NAME
|
||||
|
||||
#Clear
|
||||
docker rm -f golang_code_builder
|
||||
#docker rm -f golang_code_builder
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM golang:1.12.5
|
||||
FROM golang:1.12.12
|
||||
|
||||
ADD . /go/src/github.com/goharbor/harbor-scanner-clair/
|
||||
WORKDIR /go/src/github.com/goharbor/harbor-scanner-clair/
|
||||
|
@ -23,7 +23,7 @@ TEMP=`mktemp -d ${TMPDIR-/tmp}/clair-adapter.XXXXXX`
|
||||
git clone https://github.com/danielpacak/harbor-scanner-clair.git $TEMP
|
||||
cd $TEMP; git checkout $VERSION; cd -
|
||||
|
||||
echo 'build the clair adapter binary bases on the golang:1.12.5...'
|
||||
echo 'build the clair adapter binary bases on the golang:1.12.12'
|
||||
cp Dockerfile.binary $TEMP
|
||||
docker build -f $TEMP/Dockerfile.binary -t clair-adapter-golang $TEMP
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM golang:1.11.2
|
||||
FROM golang:1.12.12
|
||||
|
||||
ADD . /go/src/github.com/coreos/clair/
|
||||
WORKDIR /go/src/github.com/coreos/clair/
|
||||
|
@ -23,7 +23,7 @@ TEMP=`mktemp -d /$TMPDIR/clair.XXXXXX`
|
||||
git clone https://github.com/coreos/clair.git $TEMP
|
||||
cd $TEMP; git checkout $VERSION; cd -
|
||||
|
||||
echo 'build the clair binary bases on the golang:1.11.2...'
|
||||
echo 'build the clair binary bases on the golang:1.12.12'
|
||||
cp Dockerfile.binary $TEMP
|
||||
docker build -f $TEMP/Dockerfile.binary -t clair-golang $TEMP
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM golang:1.11.2
|
||||
FROM golang:1.12.12
|
||||
|
||||
ARG NOTARY_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:/migrations binary/
|
||||
|
||||
sed -i 's/waiting for $DB_URL/waiting for database/g' binary/migrations/migrate.sh
|
||||
|
||||
docker rm -f $ID
|
||||
docker rmi -f notary-binary
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM golang:1.11
|
||||
FROM golang:1.12.12
|
||||
|
||||
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
|
||||
ENV BUILDTAGS include_oss include_gcs
|
||||
|
@ -29,7 +29,7 @@ wget https://github.com/docker/distribution/pull/2879.patch
|
||||
git apply 2879.patch
|
||||
cd $cur
|
||||
|
||||
echo 'build the registry binary bases on the golang:1.11...'
|
||||
echo 'build the registry binary ...'
|
||||
cp Dockerfile.binary $TEMP
|
||||
docker build -f $TEMP/Dockerfile.binary -t registry-golang $TEMP
|
||||
|
||||
|
@ -18,6 +18,7 @@ package metadata
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -139,12 +140,12 @@ type Int64Type struct {
|
||||
}
|
||||
|
||||
func (t *Int64Type) validate(str string) error {
|
||||
_, err := strconv.ParseInt(str, 10, 64)
|
||||
_, err := parseInt64(str)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Int64Type) get(str string) (interface{}, error) {
|
||||
return strconv.ParseInt(str, 10, 64)
|
||||
return parseInt64(str)
|
||||
}
|
||||
|
||||
// BoolType ...
|
||||
@ -194,7 +195,7 @@ type QuotaType struct {
|
||||
}
|
||||
|
||||
func (t *QuotaType) validate(str string) error {
|
||||
val, err := strconv.ParseInt(str, 10, 64)
|
||||
val, err := parseInt64(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -205,3 +206,18 @@ func (t *QuotaType) validate(str string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseInt64 returns int64 from string which support scientific notation
|
||||
func parseInt64(str string) (int64, error) {
|
||||
val, err := strconv.ParseInt(str, 10, 64)
|
||||
if err == nil {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
fval, err := strconv.ParseFloat(str, 64)
|
||||
if err == nil && fval == math.Trunc(fval) {
|
||||
return int64(fval), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("invalid int64 string: %s", str)
|
||||
}
|
||||
|
@ -15,8 +15,9 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIntType_validate(t *testing.T) {
|
||||
@ -96,3 +97,33 @@ func TestMapType_get(t *testing.T) {
|
||||
result, _ := test.get(`{"sample":"abc", "another":"welcome"}`)
|
||||
assert.Equal(t, map[string]interface{}{"sample": "abc", "another": "welcome"}, result)
|
||||
}
|
||||
|
||||
func Test_parseInt64(t *testing.T) {
|
||||
type args struct {
|
||||
str string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want int64
|
||||
wantErr bool
|
||||
}{
|
||||
{"1", args{"1"}, int64(1), false},
|
||||
{"1.0", args{"1.0"}, int64(1), false},
|
||||
{"1.1", args{"1.1"}, int64(0), true},
|
||||
{"1E2", args{"1E2"}, int64(100), false},
|
||||
{"1.073741824e+11", args{"1.073741824e+11"}, int64(107374182400), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseInt64(tt.args.str)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseInt64() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("parseInt64() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ type TagDetail struct {
|
||||
Author string `json:"author"`
|
||||
Created time.Time `json:"created"`
|
||||
Config *TagCfg `json:"config"`
|
||||
Immutable bool `json:"immutable"`
|
||||
}
|
||||
|
||||
// TagCfg ...
|
||||
|
@ -10,7 +10,10 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goharbor/harbor/src/chartserver"
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
@ -18,14 +21,10 @@ import (
|
||||
hlog "github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/label"
|
||||
|
||||
"github.com/goharbor/harbor/src/core/middlewares"
|
||||
n_event "github.com/goharbor/harbor/src/core/notifier/event"
|
||||
rep_event "github.com/goharbor/harbor/src/replication/event"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -489,6 +488,12 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R
|
||||
extInfo["operator"] = cra.SecurityCtx.GetUsername()
|
||||
extInfo["projectName"] = cra.namespace
|
||||
extInfo["chartName"] = chartDetails.Metadata.Name
|
||||
|
||||
public, err := cra.ProjectMgr.IsPublic(cra.namespace)
|
||||
if err != nil {
|
||||
hlog.Errorf("failed to check the public of project %s: %v", cra.namespace, err)
|
||||
public = false
|
||||
}
|
||||
e := &rep_event.Event{
|
||||
Type: rep_event.EventTypeChartUpload,
|
||||
Resource: &model.Resource{
|
||||
@ -496,6 +501,9 @@ func (cra *ChartRepositoryAPI) addEventContext(files []formFile, request *http.R
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: fmt.Sprintf("%s/%s", cra.namespace, chartDetails.Metadata.Name),
|
||||
Metadata: map[string]interface{}{
|
||||
"public": strconv.FormatBool(public),
|
||||
},
|
||||
},
|
||||
Vtags: []string{chartDetails.Metadata.Version},
|
||||
},
|
||||
|
@ -28,7 +28,7 @@ import (
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
|
||||
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
|
||||
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
"github.com/goharbor/harbor/src/pkg/scan/rest/v1"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
@ -45,6 +45,8 @@ import (
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
notifierEvt "github.com/goharbor/harbor/src/core/notifier/event"
|
||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||
"github.com/goharbor/harbor/src/replication"
|
||||
"github.com/goharbor/harbor/src/replication/event"
|
||||
"github.com/goharbor/harbor/src/replication/model"
|
||||
@ -283,11 +285,6 @@ func (ra *RepositoryAPI) Delete() {
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
image := fmt.Sprintf("%s:%s", repoName, t)
|
||||
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
|
||||
ra.SendInternalServerError(fmt.Errorf("failed to delete labels of image %s: %v", image, err))
|
||||
return
|
||||
}
|
||||
if err = rc.DeleteTag(t); err != nil {
|
||||
if regErr, ok := err.(*commonhttp.Error); ok {
|
||||
if regErr.Code == http.StatusNotFound {
|
||||
@ -298,6 +295,11 @@ func (ra *RepositoryAPI) Delete() {
|
||||
return
|
||||
}
|
||||
log.Infof("delete tag: %s:%s", repoName, t)
|
||||
image := fmt.Sprintf("%s:%s", repoName, t)
|
||||
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
|
||||
ra.SendInternalServerError(fmt.Errorf("failed to delete labels of image %s: %v", image, err))
|
||||
return
|
||||
}
|
||||
|
||||
go func(tag string) {
|
||||
e := &event.Event{
|
||||
@ -711,6 +713,9 @@ func assembleTag(c chan *models.TagResp, client *registry.Repository, projectID
|
||||
}
|
||||
}
|
||||
|
||||
// get immutable status
|
||||
item.Immutable = isImmutable(projectID, repository, tag)
|
||||
|
||||
c <- item
|
||||
}
|
||||
|
||||
@ -791,6 +796,21 @@ func populateAuthor(detail *models.TagDetail) {
|
||||
}
|
||||
}
|
||||
|
||||
// check whether the tag is immutable
|
||||
func isImmutable(projectID int64, repo string, tag string) bool {
|
||||
_, repoName := utils.ParseRepository(repo)
|
||||
matched, err := rule.NewRuleMatcher(projectID).Match(art.Candidate{
|
||||
Repository: repoName,
|
||||
Tag: tag,
|
||||
NamespaceID: projectID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return false
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
// GetManifests returns the manifest of a tag
|
||||
func (ra *RepositoryAPI) GetManifests() {
|
||||
repoName := ra.GetString(":splat")
|
||||
|
@ -35,4 +35,4 @@ var ChartMiddlewares = []string{CHART}
|
||||
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
||||
|
||||
// MiddlewaresLocal ...
|
||||
var MiddlewaresLocal = []string{SIZEQUOTA, COUNTQUOTA}
|
||||
var MiddlewaresLocal = []string{SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
|
||||
|
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 (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
common_util "github.com/goharbor/harbor/src/common/utils"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/interceptor"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||
"github.com/goharbor/harbor/src/pkg/art"
|
||||
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
|
||||
middlerware_err "github.com/goharbor/harbor/src/core/middlewares/util/error"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type immutableHandler struct {
|
||||
next http.Handler
|
||||
builders []interceptor.Builder
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
// New ...
|
||||
func New(next http.Handler) http.Handler {
|
||||
func New(next http.Handler, builders ...interceptor.Builder) http.Handler {
|
||||
if len(builders) == 0 {
|
||||
builders = defaultBuilders
|
||||
}
|
||||
|
||||
return &immutableHandler{
|
||||
next: next,
|
||||
builders: builders,
|
||||
next: next,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP ...
|
||||
func (rh immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if match, _, _ := util.MatchPushManifest(req); !match {
|
||||
func (rh *immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
info, ok := util.ManifestInfoFromContext(req.Context())
|
||||
if !ok {
|
||||
var err error
|
||||
info, err = util.ParseManifestInfoFromPath(req)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
|
||||
if err := interceptor.HandleRequest(req); err != nil {
|
||||
log.Warningf("Error occurred when to handle request in immutable handler: %v", err)
|
||||
if _, ok := err.(middlerware_err.ErrImmutable); ok {
|
||||
http.Error(rw, util.MarshalError("DENIED",
|
||||
fmt.Sprintf("The tag is immutable, cannot be overwrite: %v", err)), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
http.Error(rw, util.MarshalError("InternalError", fmt.Sprintf("Error occurred when to handle request in immutable handler: %v", err)),
|
||||
http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
|
||||
interceptor.HandleResponse(rw, req)
|
||||
}
|
||||
|
||||
func (rh *immutableHandler) getInterceptor(req *http.Request) (interceptor.Interceptor, error) {
|
||||
for _, builder := range rh.builders {
|
||||
interceptor, err := builder.Build(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if interceptor != nil {
|
||||
return interceptor, nil
|
||||
}
|
||||
}
|
||||
|
||||
_, repoName := common_util.ParseRepository(info.Repository)
|
||||
matched, err := rule.NewRuleMatcher(info.ProjectID).Match(art.Candidate{
|
||||
Repository: repoName,
|
||||
Tag: info.Tag,
|
||||
NamespaceID: info.ProjectID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
if !matched {
|
||||
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
|
||||
return nil, nil
|
||||
}
|
||||
|
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 (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -140,7 +141,6 @@ func (n *NotificationHandler) Post() {
|
||||
log.Errorf("failed to build image push event metadata: %v", err)
|
||||
}
|
||||
|
||||
// TODO: handle image delete event and chart event
|
||||
go func() {
|
||||
e := &rep_event.Event{
|
||||
Type: rep_event.EventTypeImagePush,
|
||||
@ -149,7 +149,9 @@ func (n *NotificationHandler) Post() {
|
||||
Metadata: &model.ResourceMetadata{
|
||||
Repository: &model.Repository{
|
||||
Name: repository,
|
||||
// TODO filling the metadata
|
||||
Metadata: map[string]interface{}{
|
||||
"public": strconv.FormatBool(pro.IsPublic()),
|
||||
},
|
||||
},
|
||||
Vtags: []string{tag},
|
||||
},
|
||||
|
@ -17,6 +17,7 @@ package gc
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/garyburd/redigo/redis"
|
||||
@ -29,7 +30,6 @@ import (
|
||||
"github.com/goharbor/harbor/src/jobservice/logger"
|
||||
"github.com/goharbor/harbor/src/pkg/types"
|
||||
"github.com/goharbor/harbor/src/registryctl/client"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -38,6 +38,7 @@ const (
|
||||
dialWriteTimeout = 10 * time.Second
|
||||
blobPrefix = "blobs::*"
|
||||
repoPrefix = "repository::*"
|
||||
uploadSizePattern = "upload:*:size"
|
||||
)
|
||||
|
||||
// GarbageCollector is the struct to run registry's garbage collection
|
||||
@ -156,15 +157,13 @@ func (gc *GarbageCollector) cleanCache() error {
|
||||
// sample of keys in registry redis:
|
||||
// 1) "blobs::sha256:1a6fd470b9ce10849be79e99529a88371dff60c60aab424c077007f6979b4812"
|
||||
// 2) "repository::library/hello-world::blobs::sha256:4ab4c602aa5eed5528a6620ff18a1dc4faef0e1ab3a5eddeddb410714478c67f"
|
||||
err = delKeys(con, blobPrefix)
|
||||
if err != nil {
|
||||
gc.logger.Errorf("failed to clean registry cache %v, pattern blobs::*", err)
|
||||
return err
|
||||
}
|
||||
err = delKeys(con, repoPrefix)
|
||||
if err != nil {
|
||||
gc.logger.Errorf("failed to clean registry cache %v, pattern repository::*", err)
|
||||
return err
|
||||
// 3) "upload:fbd2e0a3-262d-40bb-abe4-2f43aa6f9cda:size"
|
||||
patterns := []string{blobPrefix, repoPrefix, uploadSizePattern}
|
||||
for _, pattern := range patterns {
|
||||
if err := delKeys(con, pattern); err != nil {
|
||||
gc.logger.Errorf("failed to clean registry cache %v, pattern %s", err, pattern)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -209,6 +209,10 @@
|
||||
</clr-tooltip>
|
||||
</label>
|
||||
</div>
|
||||
<div class="clr-checkbox-wrapper clr-form-control" [hidden]="policyId < 0">
|
||||
<input type="checkbox" [checked]="true" id="enablePolicy" formControlName="enabled" class="clr-checkbox">
|
||||
<label for="enablePolicy" class="clr-control-label">{{'REPLICATION.ENABLED_RULE' | translate}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-center">
|
||||
|
@ -203,6 +203,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
}),
|
||||
filters: this.fb.array([]),
|
||||
enabled: true,
|
||||
deletion: false,
|
||||
override: true
|
||||
});
|
||||
@ -228,6 +229,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
},
|
||||
deletion: false,
|
||||
enabled: true,
|
||||
override: true
|
||||
});
|
||||
this.isPushMode = true;
|
||||
@ -251,6 +253,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
|
||||
dest_registry: rule.dest_registry,
|
||||
trigger: rule.trigger,
|
||||
deletion: rule.deletion,
|
||||
enabled: rule.enabled,
|
||||
override: rule.override
|
||||
});
|
||||
let filtersArray = this.getFilterArray(rule);
|
||||
|
@ -24,7 +24,7 @@
|
||||
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between jobsRow">
|
||||
<h5 class="flex-items-xs-bottom option-left-down">{{'REPLICATION.REPLICATION_EXECUTIONS' | translate}}</h5>
|
||||
<div class="row flex-items-xs-between flex-items-xs-bottom">
|
||||
<div class="row flex-items-xs-between flex-items-xs-bottom fiter-task">
|
||||
<div class="execution-select">
|
||||
<div class="select filter-tag" [hidden]="!isOpenFilterTag">
|
||||
<select (change)="doFilterJob($event)">
|
||||
|
@ -49,7 +49,10 @@
|
||||
.row-right {
|
||||
margin-left: 564px;
|
||||
}
|
||||
|
||||
.fiter-task {
|
||||
margin-left: .4rem;
|
||||
margin-top: .05rem;
|
||||
}
|
||||
.replication-row {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ComponentFixture, TestBed, async, } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement} from '@angular/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
@ -25,7 +24,13 @@ import { ChannelService } from '../channel/index';
|
||||
import { LabelPieceComponent } from "../label-piece/label-piece.component";
|
||||
import { LabelDefaultService, LabelService } from "../service/label.service";
|
||||
import { OperationService } from "../operation/operation.service";
|
||||
import { ProjectDefaultService, ProjectService, RetagDefaultService, RetagService } from "../service";
|
||||
import {
|
||||
ProjectDefaultService,
|
||||
ProjectService,
|
||||
RetagDefaultService,
|
||||
RetagService, ScanningResultDefaultService,
|
||||
ScanningResultService
|
||||
} from "../service";
|
||||
import { UserPermissionDefaultService, UserPermissionService } from "../service/permission.service";
|
||||
import { USERSTATICPERMISSION } from "../service/permission-static";
|
||||
import { of } from "rxjs";
|
||||
@ -158,6 +163,17 @@ describe('RepositoryComponent (inline template)', () => {
|
||||
let mockHasRetagImagePermission: boolean = true;
|
||||
let mockHasDeleteImagePermission: boolean = true;
|
||||
let mockHasScanImagePermission: boolean = true;
|
||||
let fakedScanningResultService = {
|
||||
getProjectScanner() {
|
||||
return of({});
|
||||
}
|
||||
};
|
||||
const permissions = [
|
||||
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
|
||||
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
|
||||
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
|
||||
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
|
||||
];
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@ -188,7 +204,8 @@ describe('RepositoryComponent (inline template)', () => {
|
||||
{ provide: LabelService, useClass: LabelDefaultService},
|
||||
{ provide: UserPermissionService, useClass: UserPermissionDefaultService},
|
||||
{ provide: ChannelService},
|
||||
{ provide: OperationService }
|
||||
{ provide: OperationService },
|
||||
{ provide: ScanningResultService, useValue: fakedScanningResultService }
|
||||
]
|
||||
});
|
||||
}));
|
||||
@ -213,16 +230,10 @@ describe('RepositoryComponent (inline template)', () => {
|
||||
|
||||
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(of(mockLabels).pipe(delay(0)));
|
||||
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(of(mockLabels1).pipe(delay(0)));
|
||||
spyOn(userPermissionService, "getPermission")
|
||||
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE )
|
||||
.and.returnValue(of(mockHasAddLabelImagePermission))
|
||||
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL )
|
||||
.and.returnValue(of(mockHasRetagImagePermission))
|
||||
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE )
|
||||
.and.returnValue(of(mockHasDeleteImagePermission))
|
||||
.withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY
|
||||
, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE)
|
||||
.and.returnValue(of(mockHasScanImagePermission));
|
||||
spyOn(userPermissionService, "hasProjectPermissions")
|
||||
.withArgs(compRepo.projectId, permissions )
|
||||
.and.returnValue(of([mockHasAddLabelImagePermission, mockHasRetagImagePermission,
|
||||
mockHasDeleteImagePermission, mockHasScanImagePermission]));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
let originalTimeout;
|
||||
|
@ -68,6 +68,7 @@ export interface Tag extends Base {
|
||||
labels: Label[];
|
||||
push_time?: string;
|
||||
pull_time?: string;
|
||||
immutable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -314,6 +315,7 @@ export interface VulnerabilitySummary {
|
||||
}
|
||||
export interface SeveritySummary {
|
||||
total: number;
|
||||
fixable: number;
|
||||
summary: {[key: string]: number};
|
||||
}
|
||||
|
||||
|
@ -73,6 +73,19 @@ export abstract class ScanningResultService {
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
abstract startScanningAll(): Observable<any>;
|
||||
|
||||
/**
|
||||
* Get scanner metadata
|
||||
* @param uuid
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
abstract getScannerMetadata(uuid: string): Observable<any>;
|
||||
|
||||
/**
|
||||
* Get project scanner
|
||||
* @param projectId
|
||||
*/
|
||||
abstract getProjectScanner(projectId: number): Observable<any>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@ -153,4 +166,14 @@ export class ScanningResultDefaultService extends ScanningResultService {
|
||||
})
|
||||
, catchError(error => observableThrowError(error)));
|
||||
}
|
||||
getScannerMetadata(uuid: string): Observable<any> {
|
||||
return this.http.get(`/api/scanners/${uuid}/metadata`)
|
||||
.pipe(map(response => response as any))
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
getProjectScanner(projectId: number): Observable<any> {
|
||||
return this.http.get(`/api/projects/${projectId}/scanner`)
|
||||
.pipe(map(response => response as any))
|
||||
.pipe(catchError(error => observableThrowError(error)));
|
||||
}
|
||||
}
|
||||
|
@ -39,11 +39,11 @@
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-6">
|
||||
<div class="vulnerability" [hidden]="hasCve">
|
||||
<div class="col-md-4 col-sm-6 margin-top-5px">
|
||||
<div class="vulnerability" [hidden]="hasCve || showStatBar">
|
||||
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="vulnerabilitySummary"></hbr-vulnerability-bar>
|
||||
</div>
|
||||
<histogram-chart *ngIf="hasCve" class="margin-top-5px" [metadata]="passMetadataToChart()" [isWhiteBackground]="true"></histogram-chart>
|
||||
<histogram-chart *ngIf="hasCve" [metadata]="passMetadataToChart()" [isWhiteBackground]="true"></histogram-chart>
|
||||
</div>
|
||||
<div *ngIf="!withAdmiral && tagDetails?.labels?.length">
|
||||
<div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div>
|
||||
|
@ -48,6 +48,7 @@ describe("TagDetailComponent (inline template)", () => {
|
||||
end_time: new Date(),
|
||||
summary: {
|
||||
total: 124,
|
||||
fixable: 50,
|
||||
summary: {
|
||||
"High": 5,
|
||||
"Low": 5
|
||||
|
@ -56,6 +56,7 @@ export class TagDetailComponent implements OnInit {
|
||||
hasVulnerabilitiesListPermission: boolean;
|
||||
hasBuildHistoryPermission: boolean;
|
||||
@Input() projectId: number;
|
||||
showStatBar: boolean = true;
|
||||
constructor(
|
||||
private tagService: TagService,
|
||||
public channel: ChannelService,
|
||||
@ -83,6 +84,7 @@ export class TagDetailComponent implements OnInit {
|
||||
&& tagDetails.scan_overview
|
||||
&& tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE]) {
|
||||
this.vulnerabilitySummary = tagDetails.scan_overview[DEFAULT_SUPPORTED_MIME_TYPE];
|
||||
this.showStatBar = false;
|
||||
}
|
||||
}
|
||||
onBack(): void {
|
||||
|
@ -93,6 +93,7 @@
|
||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-cell class="truncated flex-max-width">
|
||||
<a href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
|
||||
<span *ngIf="t.immutable" class="label label-info ml-1">{{'REPOSITORY.IMMUTABLE' | translate}}</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{sizeTransform(t.size)}}</clr-dg-cell>
|
||||
<clr-dg-cell class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
|
||||
|
@ -112,6 +112,15 @@ describe("TagComponent (inline template)", () => {
|
||||
let mockHasRetagImagePermission: boolean = true;
|
||||
let mockHasDeleteImagePermission: boolean = true;
|
||||
let mockHasScanImagePermission: boolean = true;
|
||||
const mockErrorHandler = {
|
||||
error: () => {}
|
||||
};
|
||||
const permissions = [
|
||||
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE},
|
||||
{resource: USERSTATICPERMISSION.REPOSITORY.KEY, action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL},
|
||||
{resource: USERSTATICPERMISSION.REPOSITORY_TAG.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE},
|
||||
{resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE},
|
||||
];
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@ -141,6 +150,7 @@ describe("TagComponent (inline template)", () => {
|
||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
|
||||
{ provide: LabelService, useClass: LabelDefaultService },
|
||||
{ provide: UserPermissionService, useClass: UserPermissionDefaultService },
|
||||
{ provide: mockErrorHandler, useValue: ErrorHandler },
|
||||
{ provide: OperationService },
|
||||
]
|
||||
}).compileComponents();
|
||||
@ -169,15 +179,10 @@ describe("TagComponent (inline template)", () => {
|
||||
let http: HttpClient;
|
||||
http = fixture.debugElement.injector.get(HttpClient);
|
||||
spyScanner = spyOn(http, "get").and.returnValue(of(scannerMock));
|
||||
spyOn(userPermissionService, "getPermission")
|
||||
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE )
|
||||
.and.returnValue(of(mockHasAddLabelImagePermission))
|
||||
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL )
|
||||
.and.returnValue(of(mockHasRetagImagePermission))
|
||||
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE )
|
||||
.and.returnValue(of(mockHasDeleteImagePermission))
|
||||
.withArgs(comp.projectId, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE)
|
||||
.and.returnValue(of(mockHasScanImagePermission));
|
||||
spyOn(userPermissionService, "hasProjectPermissions")
|
||||
.withArgs(comp.projectId, permissions )
|
||||
.and.returnValue(of([mockHasAddLabelImagePermission, mockHasRetagImagePermission,
|
||||
mockHasDeleteImagePermission, mockHasScanImagePermission]));
|
||||
|
||||
labelService = fixture.debugElement.injector.get(LabelService);
|
||||
|
||||
|
@ -27,7 +27,12 @@ import { catchError, debounceTime, distinctUntilChanged, finalize, map } from 'r
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { Comparator, Label, State, Tag, TagClickEvent } from "../service/interface";
|
||||
|
||||
import { RequestQueryParams, RetagService, TagService, VulnerabilitySeverity } from "../service/index";
|
||||
import {
|
||||
RequestQueryParams,
|
||||
RetagService,
|
||||
ScanningResultService,
|
||||
TagService,
|
||||
} from "../service/index";
|
||||
import { ErrorHandler } from "../error-handler/error-handler";
|
||||
import { ChannelService } from "../channel/index";
|
||||
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../shared/shared.const";
|
||||
@ -54,7 +59,6 @@ import { operateChanges, OperateInfo, OperationState } from "../operation/operat
|
||||
import { OperationService } from "../operation/operation.service";
|
||||
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
|
||||
import { errorHandler as errorHandFn } from "../shared/shared.utils";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ClrLoadingState } from "@clr/angular";
|
||||
|
||||
export interface LabelState {
|
||||
@ -160,7 +164,7 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
private ref: ChangeDetectorRef,
|
||||
private operationService: OperationService,
|
||||
private channel: ChannelService,
|
||||
private http: HttpClient
|
||||
private scanningService: ScanningResultService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@ -220,9 +224,6 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (!this.withAdmiral) {
|
||||
this.getAllLabels();
|
||||
}
|
||||
}
|
||||
|
||||
public get filterLabelPieceWidth() {
|
||||
@ -726,21 +727,24 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
return st !== VULNERABILITY_SCAN_STATUS.RUNNING;
|
||||
}
|
||||
getImagePermissionRule(projectId: number): void {
|
||||
let hasAddLabelImagePermission = this.userPermissionService.getPermission(projectId, USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.KEY,
|
||||
USERSTATICPERMISSION.REPOSITORY_TAG_LABEL.VALUE.CREATE);
|
||||
let hasRetagImagePermission = this.userPermissionService.getPermission(projectId,
|
||||
USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.PULL);
|
||||
let hasDeleteImagePermission = this.userPermissionService.getPermission(projectId,
|
||||
USERSTATICPERMISSION.REPOSITORY_TAG.KEY, USERSTATICPERMISSION.REPOSITORY_TAG.VALUE.DELETE);
|
||||
let hasScanImagePermission = this.userPermissionService.getPermission(projectId,
|
||||
USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY, USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE);
|
||||
forkJoin(hasAddLabelImagePermission, hasRetagImagePermission, hasDeleteImagePermission, hasScanImagePermission)
|
||||
.subscribe(permissions => {
|
||||
this.hasAddLabelImagePermission = permissions[0] as boolean;
|
||||
this.hasRetagImagePermission = permissions[1] as boolean;
|
||||
this.hasDeleteImagePermission = permissions[2] as boolean;
|
||||
this.hasScanImagePermission = permissions[3] as boolean;
|
||||
}, error => this.errorHandler.error(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},
|
||||
];
|
||||
this.userPermissionService.hasProjectPermissions(this.projectId, permissions).subscribe((results: Array<boolean>) => {
|
||||
this.hasAddLabelImagePermission = results[0];
|
||||
this.hasRetagImagePermission = results[1];
|
||||
this.hasDeleteImagePermission = results[2];
|
||||
this.hasScanImagePermission = results[3];
|
||||
// only has label permission
|
||||
if (this.hasAddLabelImagePermission) {
|
||||
if (!this.withAdmiral) {
|
||||
this.getAllLabels();
|
||||
}
|
||||
}
|
||||
}, error => this.errorHandler.error(error));
|
||||
}
|
||||
// Trigger scan
|
||||
scanNow(t: Tag[]): void {
|
||||
@ -759,19 +763,27 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
getProjectScanner(): void {
|
||||
this.hasEnabledScanner = false;
|
||||
this.scanBtnState = ClrLoadingState.LOADING;
|
||||
this.http.get(`/api/projects/${this.projectId}/scanner`)
|
||||
.pipe(map(response => response as any))
|
||||
.pipe(catchError(error => observableThrowError(error)))
|
||||
this.scanningService.getProjectScanner(this.projectId)
|
||||
.subscribe(response => {
|
||||
if (response && "{}" !== JSON.stringify(response) && !response.disable
|
||||
&& response.health) {
|
||||
this.hasEnabledScanner = true;
|
||||
if (response && "{}" !== JSON.stringify(response) && !response.disabled
|
||||
&& response.uuid) {
|
||||
this.getScannerMetadata(response.uuid);
|
||||
} else {
|
||||
this.scanBtnState = ClrLoadingState.ERROR;
|
||||
}
|
||||
this.scanBtnState = ClrLoadingState.SUCCESS;
|
||||
}, 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) {
|
||||
if (scanOverview) {
|
||||
return scanOverview[DEFAULT_SUPPORTED_MIME_TYPE];
|
||||
|
@ -127,6 +127,6 @@ export class HistogramChartComponent implements OnInit, AfterViewInit, DoCheck {
|
||||
});
|
||||
}
|
||||
this.max = count;
|
||||
this.scale = Math.ceil(count / 4);
|
||||
this.scale = Math.ceil(count / 40) * 10;
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
end_time: new Date(),
|
||||
summary: {
|
||||
total: 124,
|
||||
fixable: 50,
|
||||
summary: {
|
||||
"High": 5,
|
||||
"Low": 5
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid [clrDgLoading]="loading">
|
||||
<clr-dg-action-bar>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!hasScanImagePermission" (click)="scanNow()"><clr-icon shape="shield-check" size="16"></clr-icon> {{'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-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | 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 { USERSTATICPERMISSION } from "../service/permission-static";
|
||||
import { DEFAULT_SUPPORTED_MIME_TYPE, VULNERABILITY_SEVERITY } from '../utils';
|
||||
import { finalize } from "rxjs/operators";
|
||||
import { finalize, map } from "rxjs/operators";
|
||||
import { ClrLoadingState } from "@clr/angular";
|
||||
|
||||
|
||||
@Component({
|
||||
@ -22,10 +23,13 @@ export class ResultGridComponent implements OnInit {
|
||||
scanningResults: VulnerabilityItem[] = [];
|
||||
dataCache: VulnerabilityItem[] = [];
|
||||
loading: boolean = false;
|
||||
shouldShowLoading: boolean = true;
|
||||
@Input() tagId: string;
|
||||
@Input() repositoryId: string;
|
||||
@Input() projectId: number;
|
||||
hasScanImagePermission: boolean;
|
||||
hasEnabledScanner: boolean = false;
|
||||
scanBtnState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||
constructor(
|
||||
private scanningService: ScanningResultService,
|
||||
private channel: ChannelService,
|
||||
@ -39,11 +43,41 @@ export class ResultGridComponent implements OnInit {
|
||||
this.channel.tagDetail$.subscribe(tag => {
|
||||
this.loadResults(this.repositoryId, this.tagId);
|
||||
});
|
||||
|
||||
if (this.projectId) {
|
||||
this.getProjectScanner();
|
||||
}
|
||||
}
|
||||
getProjectScanner(): void {
|
||||
this.hasEnabledScanner = false;
|
||||
this.scanBtnState = ClrLoadingState.LOADING;
|
||||
this.scanningService.getProjectScanner(this.projectId)
|
||||
.subscribe(response => {
|
||||
if (response && "{}" !== JSON.stringify(response) && !response.disabled
|
||||
&& response.uuid) {
|
||||
this.getScannerMetadata(response.uuid);
|
||||
} else {
|
||||
this.scanBtnState = ClrLoadingState.ERROR;
|
||||
}
|
||||
}, error => {
|
||||
this.scanBtnState = ClrLoadingState.ERROR;
|
||||
});
|
||||
}
|
||||
getScannerMetadata(uuid: string) {
|
||||
this.scanningService.getScannerMetadata(uuid)
|
||||
.subscribe(response => {
|
||||
this.hasEnabledScanner = true;
|
||||
this.scanBtnState = ClrLoadingState.SUCCESS;
|
||||
}, error => {
|
||||
this.scanBtnState = ClrLoadingState.ERROR;
|
||||
});
|
||||
}
|
||||
|
||||
loadResults(repositoryId: string, tagId: string): void {
|
||||
this.loading = true;
|
||||
// only show loading for one time
|
||||
if (this.shouldShowLoading) {
|
||||
this.loading = true;
|
||||
this.shouldShowLoading = false;
|
||||
}
|
||||
this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId)
|
||||
.pipe(finalize(() => this.loading = false))
|
||||
.subscribe((results) => {
|
||||
|
@ -1,14 +1,15 @@
|
||||
<div class="tip-wrapper tip-position width-210">
|
||||
<clr-tooltip>
|
||||
<div clrTooltipTrigger class="tip-block">
|
||||
<ng-container *ngIf="!isNone">
|
||||
<div *ngIf="criticalCount > 0" class="tip-wrapper bar-block-critical shadow-critical width-30">{{criticalCount}}</div>
|
||||
<div *ngIf="highCount > 0" class="margin-left-5 tip-wrapper bar-block-high shadow-high width-30">{{highCount}}</div>
|
||||
<div *ngIf="mediumCount > 0" class="margin-left-5 tip-wrapper bar-block-medium shadow-medium width-30">{{mediumCount}}</div>
|
||||
<div *ngIf="lowCount > 0" class="margin-left-5 tip-wrapper bar-block-low shadow-low width-30">{{lowCount}}</div>
|
||||
<div *ngIf="negligibleCount > 0" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-30">{{negligibleCount}}</div>
|
||||
<div *ngIf="unknownCount > 0" class="margin-left-5 tip-wrapper bar-block-unknown shadow-unknown width-30">{{unknownCount}}</div>
|
||||
</ng-container>
|
||||
<div *ngIf="!isNone" class="circle-block">
|
||||
<div class="level-border" [className]="getClass()">{{vulnerabilitySummary?.severity | slice:0:1}}</div>
|
||||
<div class="black-point margin-left-5"></div>
|
||||
<span class="margin-left-5">{{total}}</span>
|
||||
<span class="margin-left-5">{{'SCANNER.TOTAL' | translate}}</span>
|
||||
<div class="black-point margin-left-10"></div>
|
||||
<span class="margin-left-5">{{fixableCount}}</span>
|
||||
<span class="margin-left-5">{{'SCANNER.FIXABLE' | translate}}</span>
|
||||
</div>
|
||||
<div *ngIf="isNone" class="margin-left-5 tip-wrapper bar-block-none shadow-none width-150">{{'VULNERABILITY.NO_VULNERABILITY' | translate }}</div>
|
||||
</div>
|
||||
<clr-tooltip-content class="w-800" [clrPosition]="'right'" [clrSize]="'lg'" *clrIfOpen>
|
||||
@ -46,6 +47,10 @@
|
||||
<div class="bar-summary bar-tooltip-fon" *ngIf="!isNone">
|
||||
<histogram-chart [isWhiteBackground]="false" [metadata]="passMetadataToChart()"></histogram-chart>
|
||||
</div>
|
||||
<div>
|
||||
<span class="bar-scanning-time">{{'SCANNER.DURATION' | translate }}</span>
|
||||
<span class="margin-left-5">{{duration()}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
||||
<span>{{completeTimestamp | date:'short'}}</span>
|
||||
|
@ -208,6 +208,9 @@ hr {
|
||||
.margin-left-5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.margin-left-10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.width-30 {
|
||||
width: 30px;
|
||||
@ -220,3 +223,53 @@ hr {
|
||||
.width-150 {
|
||||
width: 150px;
|
||||
}
|
||||
.circle-block {
|
||||
color: #575757;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
div:first-child {
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.level-border {
|
||||
border:1px solid #f8b5b4;
|
||||
}
|
||||
|
||||
.level-critical {
|
||||
background:red;
|
||||
color:#621501;
|
||||
|
||||
}
|
||||
.level-high {
|
||||
background:#e64524;
|
||||
color:#621501;
|
||||
}
|
||||
.level-medium {
|
||||
background-color: orange;
|
||||
color:#621501;
|
||||
}
|
||||
.level-low {
|
||||
background: #007CBB;
|
||||
color:#cab6b1;
|
||||
}
|
||||
.level-negligible {
|
||||
background-color: green;
|
||||
color:#bad7ba;
|
||||
}
|
||||
.level-unknown {
|
||||
background-color: grey;
|
||||
color:#bad7ba;
|
||||
}
|
||||
.black-point {
|
||||
display: inline-block;
|
||||
width: 4px;background-color: #000;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
@ -3,6 +3,18 @@ import { VulnerabilitySummary } from "../../service";
|
||||
import { VULNERABILITY_SCAN_STATUS, VULNERABILITY_SEVERITY } from "../../utils";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
const MIN = 60;
|
||||
const MIN_STR = "min ";
|
||||
const SEC_STR = "sec";
|
||||
const CLASS_MAP = {
|
||||
"CRITICAL": "level-critical",
|
||||
"HIGH": "level-high",
|
||||
"MEDIUM": "level-medium",
|
||||
"LOW": "level-low",
|
||||
"NEGLIGIBLE": "level-negligible",
|
||||
"UNKNOWN": "level-unknown"
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-result-tip-histogram',
|
||||
templateUrl: './result-tip-histogram.component.html',
|
||||
@ -52,7 +64,13 @@ export class ResultTipHistogramComponent implements OnInit {
|
||||
this.vulnerabilitySummary.summary) {
|
||||
return this.vulnerabilitySummary.summary.total;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
get fixableCount() {
|
||||
if (this.vulnerabilitySummary &&
|
||||
this.vulnerabilitySummary.summary && this.vulnerabilitySummary.summary.fixable) {
|
||||
return this.vulnerabilitySummary.summary.fixable;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -72,7 +90,21 @@ export class ResultTipHistogramComponent implements OnInit {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
duration(): string {
|
||||
if (this.vulnerabilitySummary && this.vulnerabilitySummary.duration) {
|
||||
let str = '';
|
||||
const min = Math.floor(this.vulnerabilitySummary.duration / MIN);
|
||||
if (min) {
|
||||
str += min + MIN_STR;
|
||||
}
|
||||
const sec = this.vulnerabilitySummary.duration % MIN;
|
||||
if (sec) {
|
||||
str += sec + SEC_STR;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
return '0';
|
||||
}
|
||||
get highCount(): number {
|
||||
if (this.sevSummary) {
|
||||
return this.sevSummary[VULNERABILITY_SEVERITY.HIGH];
|
||||
@ -139,6 +171,29 @@ export class ResultTipHistogramComponent implements OnInit {
|
||||
return this.total === 0;
|
||||
}
|
||||
|
||||
getClass(): string {
|
||||
if (this.vulnerabilitySummary && this.vulnerabilitySummary.severity) {
|
||||
if (this.isCritical) {
|
||||
return CLASS_MAP.CRITICAL;
|
||||
}
|
||||
if (this.isHigh) {
|
||||
return CLASS_MAP.HIGH;
|
||||
}
|
||||
if (this.isMedium) {
|
||||
return CLASS_MAP.MEDIUM;
|
||||
}
|
||||
if (this.isLow) {
|
||||
return CLASS_MAP.LOW;
|
||||
}
|
||||
if (this.isNegligible) {
|
||||
return CLASS_MAP.NEGLIGIBLE;
|
||||
}
|
||||
if (this.isUnknown) {
|
||||
return CLASS_MAP.UNKNOWN;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
passMetadataToChart() {
|
||||
return [
|
||||
{
|
||||
|
@ -20,6 +20,7 @@ describe('ResultTipComponent (inline template)', () => {
|
||||
end_time: new Date(),
|
||||
summary: {
|
||||
total: 124,
|
||||
fixable: 50,
|
||||
summary: {
|
||||
"High": 5,
|
||||
"Low": 5
|
||||
|
@ -19,7 +19,6 @@ clr-modal {
|
||||
align-items: center;
|
||||
.reset-cli {
|
||||
height: 30px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.btn-padding-less {
|
||||
padding-left: 5px;
|
||||
|
@ -374,6 +374,7 @@ export class AccountSettingsModalComponent implements OnInit, AfterViewChecked {
|
||||
}
|
||||
closeReset() {
|
||||
this.showSecretDetail = false;
|
||||
this.showGenerateCliFn();
|
||||
this.resetSecretFrom.resetForm(new ResetSecret());
|
||||
}
|
||||
}
|
||||
|
@ -65,9 +65,12 @@
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{scanner.url}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="scanner.health;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
|
||||
<ng-template #elseBlock>
|
||||
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
|
||||
<span *ngIf="scanner.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2"></span>
|
||||
<ng-template #elseBlockLoading>
|
||||
<span *ngIf="scanner.metadata;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
|
||||
<ng-template #elseBlock>
|
||||
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{!scanner.disabled}}</clr-dg-cell>
|
||||
|
@ -57,10 +57,27 @@ export class ConfigurationScannerComponent implements OnInit, OnDestroy {
|
||||
.pipe(finalize(() => this.onGoing = false))
|
||||
.subscribe(response => {
|
||||
this.scanners = response;
|
||||
this.getMetadataForAll();
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
getMetadataForAll() {
|
||||
if (this.scanners && this.scanners.length > 0) {
|
||||
this.scanners.forEach((scanner, index) => {
|
||||
if (scanner.uuid ) {
|
||||
this.scanners[index].loadingMetadata = true;
|
||||
this.configScannerService.getScannerMetadata(scanner.uuid)
|
||||
.pipe(finalize(() => this.scanners[index].loadingMetadata = false))
|
||||
.subscribe(response => {
|
||||
this.scanners[index].metadata = response;
|
||||
}, error => {
|
||||
this.scanners[index].metadata = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addNewScanner(): void {
|
||||
this.newScannerDialog.open();
|
||||
|
@ -11,7 +11,7 @@
|
||||
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
|
||||
</div>
|
||||
<clr-control-error *ngIf="!isNameValid">
|
||||
{{nameTooltip | translate}}
|
||||
<span id="name-error">{{nameTooltip | translate}}</span>
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
@ -33,7 +33,7 @@
|
||||
<span class="spinner spinner-inline" [hidden]="!checkEndpointOnGoing"></span>
|
||||
</div>
|
||||
<clr-control-error *ngIf="!isEndpointValid || showEndpointError">
|
||||
{{endpointTooltip | translate}}
|
||||
<span id="endpoint-error">{{endpointTooltip | translate}}</span>
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
@ -73,7 +73,7 @@
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<clr-control-error *ngIf="!isPasswordValid">
|
||||
{{"SCANNER.PASSWORD_REQUIRED" | translate}}
|
||||
<span id="pwd-error">{{"SCANNER.PASSWORD_REQUIRED" | translate}}</span>
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -65,9 +65,9 @@ describe('NewScannerFormComponent', () => {
|
||||
nameInput.blur();
|
||||
nameInput.dispatchEvent(new Event('blur'));
|
||||
setTimeout(() => {
|
||||
let el = fixture.nativeElement.querySelector('clr-control-error');
|
||||
let el = fixture.nativeElement.querySelector('#name-error');
|
||||
expect(el).toBeFalsy();
|
||||
}, 900);
|
||||
}, 11000);
|
||||
});
|
||||
|
||||
it('endpoint url should be valid', () => {
|
||||
@ -79,9 +79,9 @@ describe('NewScannerFormComponent', () => {
|
||||
urlInput.blur();
|
||||
urlInput.dispatchEvent(new Event('blur'));
|
||||
setTimeout(() => {
|
||||
let el = fixture.nativeElement.querySelector('clr-control-error');
|
||||
let el = fixture.nativeElement.querySelector('#endpoint-error');
|
||||
expect(el).toBeFalsy();
|
||||
}, 900);
|
||||
}, 11000);
|
||||
});
|
||||
|
||||
it('auth should be valid', () => {
|
||||
@ -96,7 +96,7 @@ describe('NewScannerFormComponent', () => {
|
||||
passwordInput.value = "12345";
|
||||
usernameInput.dispatchEvent(new Event('input'));
|
||||
passwordInput.dispatchEvent(new Event('input'));
|
||||
let el = fixture.nativeElement.querySelector('clr-control-error');
|
||||
let el = fixture.nativeElement.querySelector('#pwd-error');
|
||||
expect(el).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { ScannerMetadata } from "./scanner-metadata";
|
||||
|
||||
export class Scanner {
|
||||
name?: string;
|
||||
description?: string;
|
||||
@ -13,7 +15,8 @@ export class Scanner {
|
||||
update_time?: any;
|
||||
vendor?: string;
|
||||
version?: string;
|
||||
health?: boolean;
|
||||
metadata?: ScannerMetadata;
|
||||
loadingMetadata?: boolean;
|
||||
constructor() {
|
||||
}
|
||||
}
|
||||
|
@ -18,8 +18,13 @@
|
||||
<span id="scanner-name" class="scanner-name">{{scanner?.name}}</span>
|
||||
<button *ngIf="scanners && scanners.length > 0" id="edit-scanner" class="btn btn-primary btn-sm" (click)="open()">{{'SCANNER.EDIT' | translate}}</button>
|
||||
<span *ngIf="scanner?.disabled" class="label label-warning ml-1">{{'SCANNER.DISABLED' | translate}}</span>
|
||||
<span *ngIf="scanner?.health" class="label label-success ml-1">{{'SCANNER.HEALTHY' | translate}}</span>
|
||||
<span *ngIf="!scanner?.health" class="label label-danger ml-1">{{'SCANNER.UNHEALTHY' | translate}}</span>
|
||||
<span *ngIf="scanner?.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2"></span>
|
||||
<ng-template #elseBlockLoading>
|
||||
<span *ngIf="scanner?.metadata;else elseBlock" class="label label-success ml-1">{{'SCANNER.HEALTHY' | translate}}</span>
|
||||
<ng-template #elseBlock>
|
||||
<span class="label label-danger ml-1">{{'SCANNER.UNHEALTHY' | translate}}</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -33,29 +38,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control" *ngIf="scanner?.scanner">
|
||||
<div class="clr-form-control" *ngIf="scanner?.metadata?.scanner?.name">
|
||||
<label class="clr-control-label">{{'SCANNER.ADAPTER' | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input [ngModel]="scanner?.scanner" readonly class="clr-input width-240" type="text" id="scanner-scanner"
|
||||
<input [ngModel]="scanner?.metadata?.scanner?.name" readonly class="clr-input width-240" type="text" id="scanner-scanner"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control" *ngIf="scanner?.vendor">
|
||||
<div class="clr-form-control" *ngIf="scanner?.metadata?.scanner?.vendor">
|
||||
<label class="clr-control-label">{{'SCANNER.VENDOR' | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input [ngModel]="scanner?.vendor" readonly class="clr-input width-240" type="text" id="scanner-vendor"
|
||||
<input [ngModel]="scanner?.metadata?.scanner?.vendor" readonly class="clr-input width-240" type="text" id="scanner-vendor"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control" *ngIf="scanner?.version">
|
||||
<div class="clr-form-control" *ngIf="scanner?.metadata?.scanner?.version">
|
||||
<label class="clr-control-label">{{'SCANNER.VERSION' | translate}}</label>
|
||||
<div class="clr-control-container">
|
||||
<div class="clr-input-wrapper">
|
||||
<input [ngModel]="scanner?.version" readonly class="clr-input width-240" type="text" id="scanner-version"
|
||||
<input [ngModel]="scanner?.metadata?.scanner?.version" readonly class="clr-input width-240" type="text" id="scanner-version"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
@ -76,8 +81,13 @@
|
||||
<clr-dg-cell>{{scanner.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{scanner.url}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="scanner.health" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
|
||||
<span *ngIf="!scanner.health" class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
|
||||
<span *ngIf="scanner.loadingMetadata;else elseBlockLoading" class="spinner spinner-inline ml-2">Loading...</span>
|
||||
<ng-template #elseBlockLoading>
|
||||
<span *ngIf="scanner.metadata;else elseBlock" class="label label-success">{{'SCANNER.HEALTHY' | translate}}</span>
|
||||
<ng-template #elseBlock>
|
||||
<span class="label label-danger">{{'SCANNER.UNHEALTHY' | translate}}</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="scanner.is_default" class="label label-info">{{scanner.is_default}}</span>
|
||||
|
@ -56,11 +56,24 @@ export class ScannerComponent implements OnInit {
|
||||
.subscribe(response => {
|
||||
if (response && "{}" !== JSON.stringify(response)) {
|
||||
this.scanner = response;
|
||||
this.getScannerMetadata();
|
||||
}
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
getScannerMetadata() {
|
||||
if (this.scanner && this.scanner.uuid) {
|
||||
this.scanner.loadingMetadata = true;
|
||||
this.configScannerService.getScannerMetadata(this.scanner.uuid)
|
||||
.pipe(finalize(() => this.scanner.loadingMetadata = false))
|
||||
.subscribe(response => {
|
||||
this.scanner.metadata = response;
|
||||
}, error => {
|
||||
this.scanner.metadata = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
getScanners() {
|
||||
this.loading = true;
|
||||
this.configScannerService.getScanners()
|
||||
@ -75,6 +88,22 @@ export class ScannerComponent implements OnInit {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
getMetadataForAll() {
|
||||
if (this.scanners && this.scanners.length > 0) {
|
||||
this.scanners.forEach((scanner, index) => {
|
||||
if (scanner.uuid ) {
|
||||
this.scanners[index].loadingMetadata = true;
|
||||
this.configScannerService.getScannerMetadata(scanner.uuid)
|
||||
.pipe(finalize(() => this.scanners[index].loadingMetadata = false))
|
||||
.subscribe(response => {
|
||||
this.scanners[index].metadata = response;
|
||||
}, error => {
|
||||
this.scanners[index].metadata = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
close() {
|
||||
this.opened = false;
|
||||
this.selectedScanner = null;
|
||||
@ -87,6 +116,7 @@ export class ScannerComponent implements OnInit {
|
||||
this.selectedScanner = s;
|
||||
}
|
||||
});
|
||||
this.getMetadataForAll();
|
||||
}
|
||||
get valid(): boolean {
|
||||
return this.selectedScanner
|
||||
|
@ -622,6 +622,7 @@
|
||||
"PULL_COMMAND": "Pull Command",
|
||||
"PULL_TIME": "Pull Time",
|
||||
"PUSH_TIME": "Push Time",
|
||||
"IMMUTABLE": "Immutable",
|
||||
"MY_REPOSITORY": "My Repository",
|
||||
"PUBLIC_REPOSITORY": "Public Repository",
|
||||
"DELETION_TITLE_REPO": "Confirm Repository Deletion",
|
||||
@ -636,7 +637,7 @@
|
||||
"FILTER_FOR_REPOSITORIES": "Filter Repositories",
|
||||
"TAG": "Tag",
|
||||
"SIZE": "Size",
|
||||
"VULNERABILITY": "Vulnerability",
|
||||
"VULNERABILITY": "Vulnerabilities",
|
||||
"BUILD_HISTORY": "Build History",
|
||||
"SIGNED": "Signed",
|
||||
"AUTHOR": "Author",
|
||||
@ -1291,6 +1292,9 @@
|
||||
"ENABLED": "Enabled",
|
||||
"ENABLE": "Enable",
|
||||
"DISABLE": "Disable",
|
||||
"DELETE_SUCCESS": "Successfully deleted"
|
||||
"DELETE_SUCCESS": "Successfully deleted",
|
||||
"TOTAL": "Total",
|
||||
"FIXABLE": "Fixable",
|
||||
"DURATION": "Duration:"
|
||||
}
|
||||
}
|
||||
|
@ -623,6 +623,7 @@
|
||||
"PULL_COMMAND": "Comando Pull",
|
||||
"PULL_TIME": "Pull Time",
|
||||
"PUSH_TIME": "Push Time",
|
||||
"IMMUTABLE": "Immutable",
|
||||
"MY_REPOSITORY": "Mi Repositorio",
|
||||
"PUBLIC_REPOSITORY": "Repositorio Público",
|
||||
"DELETION_TITLE_REPO": "Confirmar Eliminación de Repositorio",
|
||||
@ -637,7 +638,7 @@
|
||||
"FILTER_FOR_REPOSITORIES": "Filtrar Repositorios",
|
||||
"TAG": "Etiqueta",
|
||||
"SIZE": "Size",
|
||||
"VULNERABILITY": "Vulnerability",
|
||||
"VULNERABILITY": "Vulnerabilities",
|
||||
"BUILD_HISTORY": "Construir Historia",
|
||||
"SIGNED": "Firmada",
|
||||
"AUTHOR": "Autor",
|
||||
@ -1288,6 +1289,9 @@
|
||||
"ENABLED": "Enabled",
|
||||
"ENABLE": "Enable",
|
||||
"DISABLE": "Disable",
|
||||
"DELETE_SUCCESS": "Successfully deleted"
|
||||
"DELETE_SUCCESS": "Successfully deleted",
|
||||
"TOTAL": "Total",
|
||||
"FIXABLE": "Fixable",
|
||||
"DURATION": "Duration:"
|
||||
}
|
||||
}
|
||||
|
@ -612,6 +612,7 @@
|
||||
"PULL_COMMAND": "Commande de Pull",
|
||||
"PULL_TIME": "Pull Time",
|
||||
"PUSH_TIME": "Push Time",
|
||||
"IMMUTABLE": "Immutable",
|
||||
"MY_REPOSITORY": "Mon Dépôt",
|
||||
"PUBLIC_REPOSITORY": "Dépôt Public",
|
||||
"DELETION_TITLE_REPO": "Confirmer la Suppresion du Dépôt",
|
||||
@ -1260,6 +1261,9 @@
|
||||
"ENABLED": "Enabled",
|
||||
"ENABLE": "Enable",
|
||||
"DISABLE": "Disable",
|
||||
"DELETE_SUCCESS": "Successfully deleted"
|
||||
"DELETE_SUCCESS": "Successfully deleted",
|
||||
"TOTAL": "Total",
|
||||
"FIXABLE": "Fixable",
|
||||
"DURATION": "Duration:"
|
||||
}
|
||||
}
|
||||
|
@ -622,6 +622,7 @@
|
||||
"PULL_COMMAND": "Comando de Pull",
|
||||
"PULL_TIME": "Pull Time",
|
||||
"PUSH_TIME": "Push Time",
|
||||
"IMMUTABLE": "Immutable",
|
||||
"MY_REPOSITORY": "Meu Repositório",
|
||||
"PUBLIC_REPOSITORY": "Repositório Público",
|
||||
"DELETION_TITLE_REPO": "Confirmar remoção de repositório",
|
||||
@ -1285,7 +1286,10 @@
|
||||
"ENABLED": "Enabled",
|
||||
"ENABLE": "Enable",
|
||||
"DISABLE": "Disable",
|
||||
"DELETE_SUCCESS": "Successfully deleted"
|
||||
"DELETE_SUCCESS": "Successfully deleted",
|
||||
"TOTAL": "Total",
|
||||
"FIXABLE": "Fixable",
|
||||
"DURATION": "Duration:"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -621,6 +621,7 @@
|
||||
"PULL_COMMAND": "İndirme Komutu",
|
||||
"PULL_TIME": "İndirme Zamanı",
|
||||
"PUSH_TIME": "Yükleme Zamanı",
|
||||
"IMMUTABLE": "Immutable",
|
||||
"MY_REPOSITORY": "Depom",
|
||||
"PUBLIC_REPOSITORY": "Genel Depo",
|
||||
"DELETION_TITLE_REPO": "Depo Silme İşlemini Onaylayın",
|
||||
@ -1290,6 +1291,9 @@
|
||||
"ENABLED": "Enabled",
|
||||
"ENABLE": "Enable",
|
||||
"DISABLE": "Disable",
|
||||
"DELETE_SUCCESS": "Successfully deleted"
|
||||
"DELETE_SUCCESS": "Successfully deleted",
|
||||
"TOTAL": "Total",
|
||||
"FIXABLE": "Fixable",
|
||||
"DURATION": "Duration:"
|
||||
}
|
||||
}
|
||||
|
@ -623,6 +623,7 @@
|
||||
"PULL_COMMAND": "Pull命令",
|
||||
"PULL_TIME": "拉取时间",
|
||||
"PUSH_TIME": "推送时间",
|
||||
"IMMUTABLE": "保留的",
|
||||
"MY_REPOSITORY": "我的仓库",
|
||||
"PUBLIC_REPOSITORY": "公共仓库",
|
||||
"DELETION_TITLE_REPO": "删除镜像仓库确认",
|
||||
@ -1287,6 +1288,9 @@
|
||||
"ENABLED": "启用",
|
||||
"ENABLE": "启用",
|
||||
"DISABLE": "禁用",
|
||||
"DELETE_SUCCESS": "删除成功"
|
||||
"DELETE_SUCCESS": "删除成功",
|
||||
"TOTAL": "总计",
|
||||
"FIXABLE": "可修复",
|
||||
"DURATION": "扫描用时:"
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class TestProjects(unittest.TestCase):
|
||||
#5. Get project quota
|
||||
quota = self.system.get_project_quota("project", TestProjects.project_test_quota_id, **ADMIN_CLIENT)
|
||||
self.assertEqual(quota[0].used["count"], 1)
|
||||
self.assertEqual(quota[0].used["storage"], 2791709)
|
||||
self.assertEqual(quota[0].used["storage"], 2789174)
|
||||
|
||||
#6. Delete repository(RA) by user(UA);
|
||||
self.repo.delete_repoitory(TestProjects.repo_name, **ADMIN_CLIENT)
|
||||
|
@ -54,6 +54,6 @@ Generate And Return Secret
|
||||
Retry Element Click ${more_btn}
|
||||
Retry Element Click ${generate_secret_btn}
|
||||
Retry Double Keywords When Error Retry Element Click ${confirm_btn} Retry Wait Until Page Not Contains Element ${confirm_btn}
|
||||
Retry Wait Until Page Contains generate CLI secret success
|
||||
Retry Wait Until Page Contains Cli secret setting is successful
|
||||
${secret}= Get Secrete By API ${url}
|
||||
[Return] ${secret}
|
@ -24,5 +24,5 @@ sudo curl -o /home/travis/gopath/src/github.com/goharbor/harbor/tests/apitests/p
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends python-dev openjdk-7-jdk libssl-dev && sudo apt-get autoremove -y && sudo rm -rf /var/lib/apt/lists/*
|
||||
sudo wget https://bootstrap.pypa.io/get-pip.py && sudo python ./get-pip.py && sudo pip install --ignore-installed urllib3 chardet requests && sudo pip install robotframework==3.0.4 robotframework-httplibrary requests dbbot robotframework-pabot --upgrade
|
||||
sudo make swagger_client
|
||||
sudo make install GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage CLARITYIMAGE=goharbor/harbor-clarity-ui-builder:1.6.0 NOTARYFLAG=true CLAIRFLAG=true CHARTFLAG=true
|
||||
sudo make install GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage CLARITYIMAGE=goharbor/harbor-clarity-ui-builder:1.6.0 NOTARYFLAG=true CLAIRFLAG=true CHARTFLAG=true
|
||||
sleep 10
|
||||
|
@ -2,5 +2,5 @@
|
||||
|
||||
set -e
|
||||
|
||||
sudo make package_online VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true CLAIRFLAG=true MIGRATORFLAG=false CHARTFLAG=true HTTPPROXY=
|
||||
sudo make package_offline VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.12.5 COMPILETAG=compile_golangimage NOTARYFLAG=true CLAIRFLAG=true MIGRATORFLAG=false CHARTFLAG=true HTTPPROXY=
|
||||
sudo make package_online VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true CLAIRFLAG=true MIGRATORFLAG=false CHARTFLAG=true HTTPPROXY=
|
||||
sudo make package_offline VERSIONTAG=dev-travis PKGVERSIONTAG=dev-travis UIVERSIONTAG=dev-travis GOBUILDIMAGE=golang:1.12.12 COMPILETAG=compile_golangimage NOTARYFLAG=true CLAIRFLAG=true MIGRATORFLAG=false CHARTFLAG=true HTTPPROXY=
|
||||
|
Loading…
Reference in New Issue
Block a user