Merge pull request #4450 from reasonerjt/scan-job-migrate

Scan job migrate
This commit is contained in:
Daniel Jiang 2018-03-21 16:43:29 +08:00 committed by GitHub
commit 8a86b67b73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
130 changed files with 4648 additions and 2352 deletions

View File

@ -2,12 +2,13 @@ workspace:
base: /drone base: /drone
path: src/github.com/vmware/harbor path: src/github.com/vmware/harbor
pipeline: clone:
clone: git:
image: plugins/git image: plugins/git
tags: true tags: true
recursive: false recursive: false
pipeline:
check-org-membership: check-org-membership:
image: 'wdc-harbor-ci.eng.vmware.com/default-project/vic-integration-test:1.44' image: 'wdc-harbor-ci.eng.vmware.com/default-project/vic-integration-test:1.44'
pull: true pull: true
@ -17,9 +18,11 @@ pipeline:
SHELL: /bin/bash SHELL: /bin/bash
secrets: secrets:
- github_automation_api_key - github_automation_api_key
- skip_check_membership
commands: commands:
- echo ${DRONE_COMMIT_AUTHOR} - echo ${DRONE_COMMIT_AUTHOR}
- /bin/bash -c '[[ ! $(curl --silent "https://api.github.com/orgs/vmware/members/${DRONE_COMMIT_AUTHOR}?access_token=$GITHUB_AUTOMATION_API_KEY") ]]' - echo $SKIP_CHECK_MEMBERSHIP
- if $SKIP_CHECK_MEMBERSHIP == true; then echo 'check-org-membership step skipped'; else /bin/bash -c '[[ ! $(curl --silent "https://api.github.com/orgs/vmware/members/${DRONE_COMMIT_AUTHOR}?access_token=$GITHUB_AUTOMATION_API_KEY") ]]'; fi
when: when:
status: success status: success

2
.gitignore vendored
View File

@ -31,6 +31,8 @@ src/ui_ng/typings/
**/*yarn-error.log.* **/*yarn-error.log.*
.idea/ .idea/
.DS_Store .DS_Store
.project
.vscode/
**/node_modules **/node_modules
**/ssl/ **/ssl/
**/proxy.config.json **/proxy.config.json

View File

@ -29,6 +29,11 @@ Pull requests (PR) are always welcome, even they are small fixes like typos or a
Please submit a PR to contain changes bit by bit. A PR consisting of a lot features and code changes may be hard to review. It is recommended to submit PRs in a incremental fasion. Please submit a PR to contain changes bit by bit. A PR consisting of a lot features and code changes may be hard to review. It is recommended to submit PRs in a incremental fasion.
If you are not a member of `vmware` org in github, then your PR Drone CI build may fail. In that case, request one of the existing members / reviewers to fork your failed build to skip membership checking.
```shell
drone build start --param SKIP_CHECK_MEMBERSHIP=true vmware/harbor <Build Number>
```
### Design new features ### Design new features
You can propose new designs for existing Harbor features. You can also design You can propose new designs for existing Harbor features. You can also design

View File

@ -1,7 +1,7 @@
name: harbor name: harbor
version: 0.0.1 version: 0.1.0
appVersion: 1.3.0 appVersion: 1.4.0
description: An Enterprise-class Docker Registry Harbor by VMware description: An Enterprise-class Docker Registry by VMware
keywords: keywords:
- vmware - vmware
- docker - docker
@ -10,8 +10,10 @@ keywords:
home: https://github.com/vmware/harbor home: https://github.com/vmware/harbor
icon: https://github.com/vmware/harbor/blob/master/docs/img/harbor_logo.png icon: https://github.com/vmware/harbor/blob/master/docs/img/harbor_logo.png
sources: sources:
- https://github.com/vmware/harbor - https://github.com/vmware/harbor/tree/master/contrib/helm/harbor
maintainers: maintainers:
- name: Jesse Hu
email: huh@vmware.com
- name: paulczar - name: paulczar
email: username.taken@gmail.com email: username.taken@gmail.com
engine: gotpl engine: gotpl

View File

@ -1,27 +1,76 @@
# Project Harbor by VMware # Helm Chart for Harbor
[Harbor](http://vmware.github.io/harbor/) is an enterprise-class registry server that stores and distributes Docker images. Harbor extends the open source Docker Distribution by adding the functionalities usually required by an enterprise, such as security, identity and management. As an enterprise private registry, Harbor offers better performance and security. Having a registry closer to the build and run environment improves the image transfer efficiency. Harbor supports the setup of multiple registries and has images replicated between them. In addition, Harbor offers advanced security features, such as user management, access control and activity auditing.
## Introduction ## Introduction
This is an experimental monolithic chart that installs and configures VMWare Harbor and its dependencies. The initial implementation of this includes all of the components required to run Harbor. As upstream harbor becomes more cloud native we will be able to break apart the monolith and utitlize helm dependencies. This [Helm](https://github.com/kubernetes/helm) chart installs [Harbor](http://vmware.github.io/harbor/) in a Kubernetes cluster.
## Prerequisites ## Prerequisites
- Kubernetes 1.7+ with Beta APIs enabled - Kubernetes cluster 1.8+ with Beta APIs enabled
- Kubernetes Ingress Controller is enabled
- kubectl CLI 1.8+
- PV provisioner support in the underlying infrastructure - PV provisioner support in the underlying infrastructure
## Setup a Kubernetes cluster
You can use any tools to setup a K8s cluster.
In this guide, we use [minikube](https://github.com/kubernetes/minikube) to setup a K8s cluster as the dev/test env.
```bash
# Start minikube
minikube start --vm-driver=none
# Enable Ingress Controller
minikube addons enable ingress
```
## Installing the Chart ## Installing the Chart
To install the chart with the release name `my-release`: First install [Helm CLI](https://github.com/kubernetes/helm#install), then initialize Helm.
```bash ```bash
$ git clone https://github.com/vmware/harbor.git helm init --canary-image
$ cd harbor/contrib/helm/harbor
$ helm install --name my-release incubator/harbor
``` ```
The command deploys Harbor on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. Download Harbor helm chart code.
```bash
git clone https://github.com/vmware/harbor
cd harbor/contrib/helm/harbor
```
### Insecure Registry Mode
If setting Harbor Registry as insecure-registries for docker,
you don't need to generate Root CA and SSL certificate for the Harbor ingress controller.
Install the Harbor helm chart with a release name `my-release`:
```bash
helm install . --debug --name my-release --set externalDomain=harbor.my.domain,insecureRegistry=true
```
**Make sure** `harbor.my.domain` resolves to the K8s Ingress Controller IP on the machines where you run docker or access Harbor UI.
You can add `harbor.my.domain` and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN `harbor.<IP>.xip.io`.
Then add `"insecure-registries": ["harbor.my.domain"]` in the docker daemon config file and restart docker service.
### Secure Registry Mode
By default this chart will generate a root CA and SSL certificate for your Harbor.
You can also use your own CA signed certificate:
open values.yaml, set the value of 'externalDomain' to your Harbor FQDN, and
set value of 'tlsCrt', 'tlsKey', 'caCrt'. The common name of the certificate must match your Harbor FQDN.
Install the Harbor helm chart with a release name `my-release`:
```bash
helm install . --debug --name my-release --set externalDomain=harbor.my.domain
```
Follow the `NOTES` section in the command output to get Harbor admin password and **add Harbor root CA into docker trusted certificates**.
The command deploys Harbor on the Kubernetes cluster in the default configuration.
The [configuration](#configuration) section lists the parameters that can be configured during installation.
> **Tip**: List all releases using `helm list` > **Tip**: List all releases using `helm list`
@ -30,26 +79,29 @@ The command deploys Harbor on the Kubernetes cluster in the default configuratio
To uninstall/delete the `my-release` deployment: To uninstall/delete the `my-release` deployment:
```bash ```bash
$ helm delete my-release helm delete my-release
``` ```
The command removes all the Kubernetes components associated with the chart and deletes the release. The command removes all the Kubernetes components associated with the chart and deletes the release.
## Configuration ## Configuration
The following tables lists the configurable parameters of the Percona chart and their default values. The following tables lists the configurable parameters of the Harbor chart and the default values.
| Parameter | Description | Default | | Parameter | Description | Default |
| ----------------------- | ---------------------------------- | ----------------------- | | ----------------------- | ---------------------------------- | ----------------------- |
| **Harbor** | | **Harbor** |
| `externalDomain` | domain harbor will run on (https://*harbor.url*/) |`harbor.192.168.99.100.xip.io` | | `harborImageTag` | The tag for Harbor docker images | `v1.4.0` |
| `tls_crt` | TLS certificate to use for Harbor's https endpoint | see values.yaml | | `externalDomain` | Harbor will run on (https://`externalDomain`/). Recommend using K8s Ingress Controller FQDN as `externalDomain`, or make sure this FQDN resolves to the K8s Ingress Controller IP. | `harbor.my.domain` |
| `tls_key` | TLS key to use for Harbor's https endpoint | see values.yaml | | `insecureRegistry` | If set to true, you don't need to set tlsCrt/tlsKey/caCrt, but must add Harbor FQDN as insecure-registries for your docker client. | `false` |
| `ca_crt` | CA Cert for self signed TLS cert | see values.yaml | | `tlsCrt` | TLS certificate to use for Harbor's https endpoint. Its CN must match `externalDomain`. | auto-generated |
| `tlsKey` | TLS key to use for Harbor's https endpoint | auto-generated |
| `caCrt` | CA Cert for self signed TLS cert | auto-generated |
| `persistence.enabled` | enable persistent data storage | `false` | | `persistence.enabled` | enable persistent data storage | `false` |
| `secretKey` | The secret key used for encryption. Must be a string of 16 chars. | `not-a-secure-key` |
| **Adminserver** | | **Adminserver** |
| `adminserver.image.repository` | Repository for adminserver image | `vmware/harbor-adminserver` | | `adminserver.image.repository` | Repository for adminserver image | `vmware/harbor-adminserver` |
| `adminserver.image.tag` | Tag for adminserver image | `v1.3.0` | | `adminserver.image.tag` | Tag for adminserver image | `v1.4.0` |
| `adminserver.image.pullPolicy` | Pull Policy for adminserver image | `IfNotPresent` | | `adminserver.image.pullPolicy` | Pull Policy for adminserver image | `IfNotPresent` |
| `adminserver.emailHost` | email server | `smtp.mydomain.com` | | `adminserver.emailHost` | email server | `smtp.mydomain.com` |
| `adminserver.emailPort` | email port | `25` | | `adminserver.emailPort` | email port | `25` |
@ -64,14 +116,14 @@ The following tables lists the configurable parameters of the Percona chart and
| `adminserver.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml | | `adminserver.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml |
| **Jobservice** | | **Jobservice** |
| `jobservice.image.repository` | Repository for jobservice image | `vmware/harbor-jobservice` | | `jobservice.image.repository` | Repository for jobservice image | `vmware/harbor-jobservice` |
| `jobservice.image.tag` | Tag for jobservice image | `v1.3.0` | | `jobservice.image.tag` | Tag for jobservice image | `v1.4.0` |
| `jobservice.image.pullPolicy` | Pull Policy for jobservice image | `IfNotPresent` | | `jobservice.image.pullPolicy` | Pull Policy for jobservice image | `IfNotPresent` |
| `jobservice.key` | jobservice key | `not-a-secure-key` | | `jobservice.key` | jobservice key | `not-a-secure-key` |
| `jobservice.secret` | jobservice secret | `not-a-secure-secret` | | `jobservice.secret` | jobservice secret | `not-a-secure-secret` |
| `jobservice.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | `jobservice.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined |
| **UI** | | **UI** |
| `ui.image.repository` | Repository for ui image | `vmware/harbor-ui` | | `ui.image.repository` | Repository for ui image | `vmware/harbor-ui` |
| `ui.image.tag` | Tag for ui image | `v1.3.0` | | `ui.image.tag` | Tag for ui image | `v1.4.0` |
| `ui.image.pullPolicy` | Pull Policy for ui image | `IfNotPresent` | | `ui.image.pullPolicy` | Pull Policy for ui image | `IfNotPresent` |
| `ui.key` | ui key | `not-a-secure-key` | | `ui.key` | ui key | `not-a-secure-key` |
| `ui.secret` | ui secret | `not-a-secure-secret` | | `ui.secret` | ui secret | `not-a-secure-secret` |
@ -79,7 +131,7 @@ The following tables lists the configurable parameters of the Percona chart and
| `ui.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | `ui.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined |
| **MySQL** | | **MySQL** |
| `mysql.image.repository` | Repository for mysql image | `vmware/harbor-mysql` | | `mysql.image.repository` | Repository for mysql image | `vmware/harbor-mysql` |
| `mysql.image.tag` | Tag for mysql image | `v1.3.0` | | `mysql.image.tag` | Tag for mysql image | `v1.4.0` |
| `mysql.image.pullPolicy` | Pull Policy for mysql image | `IfNotPresent` | | `mysql.image.pullPolicy` | Pull Policy for mysql image | `IfNotPresent` |
| `mysql.host` | MySQL Server | `~` | | `mysql.host` | MySQL Server | `~` |
| `mysql.port` | MySQL Port | `3306` | | `mysql.port` | MySQL Port | `3306` |
@ -89,22 +141,18 @@ The following tables lists the configurable parameters of the Percona chart and
| `mysql.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | `mysql.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined |
| `mysql.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml | | `mysql.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml |
| **Registry** | | **Registry** |
| `registry.image.repository` | Repository for registry image | `vmware/harbor-registry` | | `registry.image.repository` | Repository for registry image | `vmware/registry-photon` |
| `registry.image.tag` | Tag for registry image | `v1.3.0` | | `registry.image.tag` | Tag for registry image | `v2.6.2-v1.4.0` |
| `registry.image.pullPolicy` | Pull Policy for registry image | `IfNotPresent` | | `registry.image.pullPolicy` | Pull Policy for registry image | `IfNotPresent` |
| `registry.rootCrt` | registry root cert | see values.yaml | | `registry.rootCrt` | registry root cert | see values.yaml |
| `registry.httpSecret` | registry secret | `not-a-secure-secret` | | `registry.httpSecret` | registry secret | `not-a-secure-secret` |
| `registry.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | `registry.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined |
| `registry.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml | | `registry.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml |
| **Clair** | | **Clair** |
| `clair.enabled` | Enable clair? | `false` | | `clair.enabled` | Enable clair? | `true` |
| `clair.image.repository` | Repository for clair image | `vmware/clair-photon` |
| `clair.image.tag` | Tag for clair image | `v2.0.1-v1.4.0`
| `clair.postgresPassword` | password for clair postgres | see values.yaml | | `clair.postgresPassword` | password for clair postgres | see values.yaml |
| `clair.image.repository` | Repository for clair image | `vmware/clair` |
| `clair.image.tag` | Tag for clair image | `v2.0.1-photon` |
| `clair.image.pullPolicy` | Pull Policy for clair image | `IfNotPresent` |
| `clair.pgImage.repository` | Repository for clair postgres image | `postgres` |
| `clair.pgImage.tag` | Tag for clair postgres image | `9.6.4` |
| `clair.pgImage.pullPolicy` | Pull Policy for clair postgres image | `IfNotPresent` |
| `clair.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | `clair.pgResources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | `clair.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | `clair.pgResources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined |
| | | | | | | |
@ -112,19 +160,19 @@ The following tables lists the configurable parameters of the Percona chart and
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example: Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example:
```bash ```bash
$ helm install --name my-release --set mysql.pass=baconeggs . helm install --name my-release --set mysql.pass=baconeggs .
``` ```
Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example,
```bash ```bash
$ helm install --name my-release -f /path/to/values.yaml . helm install --name my-release -f /path/to/values.yaml .
``` ```
> **Tip**: You can use the default [values.yaml](values.yaml) > **Tip**: You can use the default [values.yaml](values.yaml)
## Persistence ## Persistence
VMWare Harbor stores the data and configurations in emptyDir volumes. You can change the values.yaml to enable persistence and use a PersistentVolumeClaim instead. Harbor stores the data and configurations in emptyDir volumes. You can change the values.yaml to enable persistence and use a PersistentVolumeClaim instead.
> *"An emptyDir volume is first created when a Pod is assigned to a Node, and exists as long as that Pod is running on that node. When a Pod is removed from a node for any reason, the data in the emptyDir is deleted forever."* > *"An emptyDir volume is first created when a Pod is assigned to a Node, and exists as long as that Pod is running on that node. When a Pod is removed from a node for any reason, the data in the emptyDir is deleted forever."*

View File

@ -1,15 +1,20 @@
To add the CA certificate to docker copy the contents of the following command into /etc/docker/certs.d/{{ .Values.externalDomain }}:
$ kubectl get secret \ Add the Harbor CA certificate to Docker by executing the following command:
sudo mkdir -p /etc/docker/certs.d/{{ .Values.externalDomain }}
kubectl get secret \
--namespace {{ .Release.Namespace }} {{ template "harbor.fullname" . }}-ingress \ --namespace {{ .Release.Namespace }} {{ template "harbor.fullname" . }}-ingress \
-o jsonpath="{.data.ca\.crt}" | base64 --decode -o jsonpath="{.data.ca\.crt}" | base64 --decode | \
sudo tee /etc/docker/certs.d/{{ .Values.externalDomain }}/ca.crt
Access Harbor via: https://{{ .Values.externalDomain }} Get Harbor admin password by executing the following command:
login to harbor with docker cli: kubectl get secret --namespace {{ .Release.Namespace }} {{ template "harbor.fullname" . }}-adminserver -o jsonpath="{.data.HARBOR_ADMIN_PASSWORD}" | base64 --decode; echo
docker login {{ .Values.externalDomain }} Add Harbor FQDN {{ .Values.externalDomain }} to K8s Ingress Controller IP resolution on DNS Server or in file /etc/hosts.
To get your admin password run the following (not yet ready): Access Harbor UI via https://{{ .Values.externalDomain }}
$ kubectl get secret --namespace {{ .Release.Namespace }} {{ template "harbor.fullname" . }} -o jsonpath="{.data.}" | base64 --decode; echo Login Harbor with Docker CLI:
docker login {{ .Values.externalDomain }}

View File

@ -17,7 +17,7 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this
{{- end -}} {{- end -}}
{{/* Helm required labels */}} {{/* Helm required labels */}}
{{- define "helm.labels" -}} {{- define "harbor.labels" -}}
heritage: {{ .Release.Service }} heritage: {{ .Release.Service }}
release: {{ .Release.Name }} release: {{ .Release.Name }}
chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
@ -25,7 +25,7 @@ app: "{{ template "harbor.name" . }}"
{{- end -}} {{- end -}}
{{/* matchLabels */}} {{/* matchLabels */}}
{{- define "helm.matchLabels" -}} {{- define "harbor.matchLabels" -}}
release: {{ .Release.Name }} release: {{ .Release.Name }}
app: "{{ template "harbor.name" . }}" app: "{{ template "harbor.name" . }}"
{{- end -}} {{- end -}}

View File

@ -3,7 +3,8 @@ kind: ConfigMap
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-adminserver" name: "{{ template "harbor.fullname" . }}-adminserver"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: adminserver
data: data:
{{ if .Values.mysql.host -}} {{ if .Values.mysql.host -}}
MYSQL_HOST: "{{ .Values.mysql.host }}" MYSQL_HOST: "{{ .Values.mysql.host }}"
@ -19,21 +20,26 @@ data:
EMAIL_SSL: "{{ .Values.adminserver.emailSsl }}" EMAIL_SSL: "{{ .Values.adminserver.emailSsl }}"
EMAIL_FROM: "{{ .Values.adminserver.emailFrom }}" EMAIL_FROM: "{{ .Values.adminserver.emailFrom }}"
EMAIL_IDENTITY: "{{ .Values.adminserver.emailIdentity }}" EMAIL_IDENTITY: "{{ .Values.adminserver.emailIdentity }}"
EMAIL_INSECURE: "{{ .Values.adminserver.emailInsecure }}"
EXT_ENDPOINT: "https://{{ .Values.externalDomain }}" EXT_ENDPOINT: "https://{{ .Values.externalDomain }}"
UI_URL: "http://{{ template "harbor.fullname" . }}-ui"
JOBSERVICE_URL: "http://{{ template "harbor.fullname" . }}-jobservice"
REGISTRY_URL: "http://{{ template "harbor.fullname" . }}-registry:5000" REGISTRY_URL: "http://{{ template "harbor.fullname" . }}-registry:5000"
TOKEN_SERVICE_URL: "http://{{ template "harbor.fullname" . }}-ui/service/token" TOKEN_SERVICE_URL: "http://{{ template "harbor.fullname" . }}-ui/service/token"
WITH_NOTARY: "{{ .Values.notary.enabled }}" WITH_NOTARY: "{{ .Values.notary.enabled }}"
LOG_LEVEL: "info" LOG_LEVEL: "info"
IMAGE_STORE_PATH: "/" IMAGE_STORE_PATH: "/" # This is a temporary hack.
AUTH_MODE: "database" AUTH_MODE: "db_auth"
SELF_REGISTRATION: "on" SELF_REGISTRATION: "on"
LDAP_URL: "ldaps://ldapserver" LDAP_URL: "ldaps://ldapserver"
LDAP_SEARCH_DN: "" LDAP_SEARCH_DN: ""
LDAP_BASE_DN: "" LDAP_BASE_DN: ""
LDAP_FILTER: "(objectClass=person)" LDAP_FILTER: "(objectClass=person)"
LDAP_UID: "uid" LDAP_UID: "uid"
LDAP_SCOPE: "3" LDAP_SCOPE: "2"
LDAP_TIMEOUT: "5" LDAP_TIMEOUT: "5"
LDAP_TIMEOUT: "5"
LDAP_VERIFY_CERT: "True"
DATABASE_TYPE: "mysql" DATABASE_TYPE: "mysql"
PROJECT_CREATION_RESTRICTION: "everyone" PROJECT_CREATION_RESTRICTION: "everyone"
VERIFY_REMOTE_CERT: "off" VERIFY_REMOTE_CERT: "off"
@ -45,3 +51,12 @@ data:
RESET: "false" RESET: "false"
WITH_CLAIR: "{{ .Values.clair.enabled }}" WITH_CLAIR: "{{ .Values.clair.enabled }}"
CLAIR_DB_HOST: "{{ template "harbor.fullname" . }}-clair-pg" CLAIR_DB_HOST: "{{ template "harbor.fullname" . }}-clair-pg"
CLAIR_DB_PORT: "5432"
CLAIR_DB: "postgres"
CLAIR_DB_USERNAME: "postgres"
CLAIR_DB_PASSWORD: "{{ .Values.clair.postgresPassword }}"
UAA_ENDPOINT: ""
UAA_CLIENTID: ""
UAA_CLIENTSECRET: ""
UAA_VERIFY_CERT: "True"
REGISTRY_STORAGE_PROVIDER_NAME: "filesystem"

View File

@ -3,10 +3,11 @@ kind: Secret
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-adminserver" name: "{{ template "harbor.fullname" . }}-adminserver"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: adminserver
type: Opaque type: Opaque
data: data:
key: {{ .Values.adminserver.key | b64enc | quote }} secretKey: {{ .Values.secretKey | b64enc | quote }}
EMAIL_PWD: {{ .Values.adminserver.emailPwd | b64enc | quote }} EMAIL_PWD: {{ .Values.adminserver.emailPwd | b64enc | quote }}
HARBOR_ADMIN_PASSWORD: {{ .Values.adminserver.harborAdminPassword | b64enc | quote }} HARBOR_ADMIN_PASSWORD: {{ .Values.adminserver.harborAdminPassword | b64enc | quote }}
MYSQL_PWD: {{ .Values.mysql.pass | b64enc | quote }} MYSQL_PWD: {{ .Values.mysql.pass | b64enc | quote }}

View File

@ -3,19 +3,20 @@ kind: StatefulSet
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-adminserver" name: "{{ template "harbor.fullname" . }}-adminserver"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: adminserver component: adminserver
spec: spec:
replicas: 1 replicas: 1
serviceName: "{{ template "harbor.fullname" . }}" serviceName: "{{ template "harbor.fullname" . }}-adminserver"
selector: selector:
matchLabels: matchLabels:
{{ include "helm.matchLabels" . | indent 6 }} {{ include "harbor.matchLabels" . | indent 6 }}
component: adminserver component: adminserver
template: template:
metadata: metadata:
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.labels" . | indent 8 }}
component: adminserver
component: adminserver component: adminserver
spec: spec:
containers: containers:
@ -53,7 +54,7 @@ spec:
secret: secret:
secretName: "{{ template "harbor.fullname" . }}-adminserver" secretName: "{{ template "harbor.fullname" . }}-adminserver"
items: items:
- key: key - key: secretKey
path: key path: key
{{- if .Values.persistence.enabled }} {{- if .Values.persistence.enabled }}
volumeClaimTemplates: volumeClaimTemplates:

View File

@ -2,12 +2,10 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-adminserver" name: "{{ template "harbor.fullname" . }}-adminserver"
labels:
{{ include "helm.labels" . | indent 4 }}
spec: spec:
ports: ports:
- port: 80 - port: 80
targetPort: 8080 targetPort: 8080
selector: selector:
{{ include "helm.matchLabels" . | indent 4 }} {{ include "harbor.matchLabels" . | indent 4 }}
component: adminserver component: adminserver

View File

@ -4,7 +4,8 @@ kind: ConfigMap
metadata: metadata:
name: {{ template "harbor.fullname" . }} name: {{ template "harbor.fullname" . }}
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: clair
data: data:
config.yaml: | config.yaml: |
clair: clair:

View File

@ -4,14 +4,18 @@ kind: Deployment
metadata: metadata:
name: {{ template "harbor.fullname" . }}-clair name: {{ template "harbor.fullname" . }}-clair
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: clair component: clair
spec: spec:
replicas: 1 replicas: 1
selector:
matchLabels:
{{ include "harbor.matchLabels" . | indent 6 }}
component: clair
template: template:
metadata: metadata:
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.labels" . | indent 8 }}
component: clair component: clair
spec: spec:
containers: containers:

View File

@ -1,19 +1,4 @@
{{ if .Values.clair.enabled }} {{ if .Values.clair.enabled }}
apiVersion: v1
kind: Service
metadata:
name: "{{ template "harbor.fullname" . }}-clair"
labels:
{{ include "helm.labels" . | indent 4 }}
spec:
ports:
- port: 6060
selector:
app: "{{ template "harbor.fullname" . }}"
component: adminserver
release: {{ .Release.Name }}
---
---
# clair host isn't configurable yet. this creates a service # clair host isn't configurable yet. this creates a service
# to get it working for now. # to get it working for now.
# see https://github.com/vmware/harbor/issues/3250 # see https://github.com/vmware/harbor/issues/3250
@ -21,11 +6,12 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: clair name: clair
labels:
{{ include "harbor.labels" . | indent 4 }}
spec: spec:
ports: ports:
- port: 6060 - port: 6060
selector: selector:
app: "{{ template "harbor.fullname" . }}" {{ include "harbor.matchLabels" . | indent 4 }}
component: adminserver component: clair
release: {{ .Release.Name }}
{{ end }} {{ end }}

View File

@ -4,7 +4,7 @@ kind: Secret
metadata: metadata:
name: {{ template "harbor.fullname" . }}-clair-pg-config name: {{ template "harbor.fullname" . }}-clair-pg-config
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
type: Opaque type: Opaque
data: data:
POSTGRES_PASSWORD: {{ .Values.clair.postgresPassword | b64enc | quote }} POSTGRES_PASSWORD: {{ .Values.clair.postgresPassword | b64enc | quote }}

View File

@ -4,19 +4,19 @@ kind: StatefulSet
metadata: metadata:
name: {{ template "harbor.fullname" . }}-clair-pg name: {{ template "harbor.fullname" . }}-clair-pg
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: clair-pg component: clair-pg
spec: spec:
serviceName: "{{ template "harbor.fullname" . }}-clair-pg" serviceName: "{{ template "harbor.fullname" . }}-clair-pg"
selector: selector:
matchLabels: matchLabels:
{{ include "helm.matchLabels" . | indent 6 }} {{ include "harbor.matchLabels" . | indent 6 }}
component: clair-pg component: clair-pg
template: template:
metadata: metadata:
name: {{ template "harbor.fullname" . }}-clair-pg name: {{ template "harbor.fullname" . }}-clair-pg
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.labels" . | indent 8 }}
component: clair-pg component: clair-pg
spec: spec:
containers: containers:
@ -55,7 +55,7 @@ spec:
- metadata: - metadata:
name: pgdata name: pgdata
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.labels" . | indent 8 }}
spec: spec:
accessModes: [{{ .Values.clair.volumes.pgData.accessMode | quote }}] accessModes: [{{ .Values.clair.volumes.pgData.accessMode | quote }}]
{{- if .Values.clair.volumes.pgData.storageClass }} {{- if .Values.clair.volumes.pgData.storageClass }}

View File

@ -4,27 +4,11 @@ kind: Service
metadata: metadata:
name: {{ template "harbor.fullname" . }}-clair-pg name: {{ template "harbor.fullname" . }}-clair-pg
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
spec: spec:
ports: ports:
- port: 5432 - port: 5432
selector: selector:
{{ include "helm.matchLabels" . | indent 4 }} {{ include "harbor.matchLabels" . | indent 4 }}
component: clair-pg
---
# clairdb host isn't configurable yet. this creates a service
# to get it working for now.
# see https://github.com/vmware/harbor/commit/f63588855f8d3b1b138d3be63ca165bb52ab930c
apiVersion: v1
kind: Service
metadata:
name: postgres
labels:
{{ include "helm.labels" . | indent 4 }}
spec:
ports:
- port: 5432
selector:
{{ include "helm.matchLabels" . | indent 4 }}
component: clair-pg component: clair-pg
{{ end }} {{ end }}

View File

@ -3,16 +3,16 @@ kind: Ingress
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-ingress" name: "{{ template "harbor.fullname" . }}-ingress"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
annotations: annotations:
ingress.kubernetes.io/ssl-redirect: "true" {{ toYaml .Values.ingress.annotations | indent 4 }}
ingress.kubernetes.io/body-size: "0"
ingress.kubernetes.io/proxy-body-size: "0"
spec: spec:
{{ if not .Values.insecureRegistry }}
tls: tls:
- hosts: - hosts:
- "{{ .Values.externalDomain }}" - "{{ .Values.externalDomain }}"
secretName: "{{ template "harbor.fullname" . }}-ingress" secretName: "{{ template "harbor.fullname" . }}-ingress"
{{ end }}
rules: rules:
- host: "{{ .Values.externalDomain }}" - host: "{{ .Values.externalDomain }}"
http: http:
@ -25,7 +25,3 @@ spec:
backend: backend:
serviceName: {{ template "harbor.fullname" . }}-registry serviceName: {{ template "harbor.fullname" . }}-registry
servicePort: 5000 servicePort: 5000
- path: /v1
backend:
serviceName: {{ template "harbor.fullname" . }}-fake-service
servicePort: 5000

View File

@ -1,11 +1,15 @@
{{ if not .Values.insecureRegistry }}
{{ $ca := genCA "harbor-ca" 365 }}
{{ $cert := genSignedCert .Values.externalDomain nil nil 365 $ca }}
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-ingress" name: "{{ template "harbor.fullname" . }}-ingress"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
type: kubernetes.io/tls type: kubernetes.io/tls
data: data:
tls.crt: {{ .Values.tlsCrt | b64enc | quote }} tls.crt: {{ .Values.tlsCrt | default $cert.Cert | b64enc | quote }}
tls.key: {{ .Values.tlsKey | b64enc | quote }} tls.key: {{ .Values.tlsKey | default $cert.Key | b64enc | quote }}
ca.crt: {{ .Values.caCrt | b64enc | quote }} ca.crt: {{ .Values.caCrt | default $ca.Cert | b64enc | quote }}
{{ end }}

View File

@ -3,7 +3,7 @@ kind: ConfigMap
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-jobservice" name: "{{ template "harbor.fullname" . }}-jobservice"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
data: data:
app.conf: |+ app.conf: |+
appname = jobservice appname = jobservice

View File

@ -3,14 +3,18 @@ kind: Deployment
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-jobservice" name: "{{ template "harbor.fullname" . }}-jobservice"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: jobservice component: jobservice
spec: spec:
replicas: 1 replicas: 1
selector:
matchLabels:
{{ include "harbor.matchLabels" . | indent 6 }}
component: jobservice
template: template:
metadata: metadata:
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.labels" . | indent 8 }}
component: jobservice component: jobservice
spec: spec:
containers: containers:
@ -50,7 +54,7 @@ spec:
secret: secret:
secretName: "{{ template "harbor.fullname" . }}-jobservice" secretName: "{{ template "harbor.fullname" . }}-jobservice"
items: items:
- key: key - key: secretKey
path: key path: key
- name: job-logs - name: job-logs
emptyDir: {} emptyDir: {}

View File

@ -3,9 +3,9 @@ kind: Secret
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-jobservice" name: "{{ template "harbor.fullname" . }}-jobservice"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
type: Opaque type: Opaque
data: data:
secretKey: {{ .Values.secretKey | b64enc | quote }}
JOBSERVICE_SECRET: {{ .Values.jobservice.secret | b64enc | quote }} JOBSERVICE_SECRET: {{ .Values.jobservice.secret | b64enc | quote }}
key: {{ .Values.jobservice.key | b64enc | quote }}
UI_SECRET: {{ .Values.ui.secret | b64enc | quote }} UI_SECRET: {{ .Values.ui.secret | b64enc | quote }}

View File

@ -3,11 +3,11 @@ kind: Service
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-jobservice" name: "{{ template "harbor.fullname" . }}-jobservice"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
spec: spec:
ports: ports:
- port: 80 - port: 80
targetPort: 8080 targetPort: 8080
selector: selector:
{{ include "helm.matchLabels" . | indent 4 }} {{ include "harbor.matchLabels" . | indent 4 }}
component: jobservice component: jobservice

View File

@ -3,7 +3,7 @@ kind: Secret
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-mysql" name: "{{ template "harbor.fullname" . }}-mysql"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
type: Opaque type: Opaque
data: data:
mysqlRootPassword: {{ .Values.mysql.pass | b64enc | quote }} mysqlRootPassword: {{ .Values.mysql.pass | b64enc | quote }}

View File

@ -3,19 +3,19 @@ kind: StatefulSet
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-mysql" name: "{{ template "harbor.fullname" . }}-mysql"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: mysql component: mysql
spec: spec:
replicas: 1 replicas: 1
serviceName: "{{ template "harbor.fullname" . }}-mysql" serviceName: "{{ template "harbor.fullname" . }}-mysql"
selector: selector:
matchLabels: matchLabels:
{{ include "helm.matchLabels" . | indent 6 }} {{ include "harbor.matchLabels" . | indent 6 }}
component: mysql component: mysql
template: template:
metadata: metadata:
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.labels" . | indent 8 }}
component: mysql component: mysql
spec: spec:
containers: containers:
@ -43,7 +43,7 @@ spec:
- metadata: - metadata:
name: "mysql-data" name: "mysql-data"
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.labels" . | indent 8 }}
spec: spec:
accessModes: [{{ .Values.mysql.volumes.data.accessMode | quote }}] accessModes: [{{ .Values.mysql.volumes.data.accessMode | quote }}]
{{- if .Values.mysql.volumes.data.storageClass }} {{- if .Values.mysql.volumes.data.storageClass }}

View File

@ -3,10 +3,10 @@ kind: Service
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-mysql" name: "{{ template "harbor.fullname" . }}-mysql"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
spec: spec:
ports: ports:
- port: 3306 - port: 3306
selector: selector:
{{ include "helm.matchLabels" . | indent 4 }} {{ include "harbor.matchLabels" . | indent 4 }}
component: mysql component: mysql

View File

@ -3,7 +3,7 @@ kind: ConfigMap
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-registry" name: "{{ template "harbor.fullname" . }}-registry"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
data: data:
config.yml: |+ config.yml: |+
version: 0.1 version: 0.1

View File

@ -3,7 +3,7 @@ kind: Secret
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-registry" name: "{{ template "harbor.fullname" . }}-registry"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
type: Opaque type: Opaque
data: data:
httpSecret: {{ .Values.registry.httpSecret | b64enc | quote }} httpSecret: {{ .Values.registry.httpSecret | b64enc | quote }}

View File

@ -3,19 +3,19 @@ kind: StatefulSet
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-registry" name: "{{ template "harbor.fullname" . }}-registry"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: registry component: registry
spec: spec:
replicas: 1 replicas: 1
serviceName: "{{ template "harbor.fullname" . }}-registry" serviceName: "{{ template "harbor.fullname" . }}-registry"
selector: selector:
matchLabels: matchLabels:
{{ include "helm.matchLabels" . | indent 6 }} {{ include "harbor.matchLabels" . | indent 6 }}
component: registry component: registry
template: template:
metadata: metadata:
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.labels" . | indent 8 }}
component: registry component: registry
spec: spec:
containers: containers:
@ -62,7 +62,7 @@ spec:
- metadata: - metadata:
name: "registry-data" name: "registry-data"
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.labels" . | indent 8 }}
spec: spec:
accessModes: [{{ .Values.registry.volumes.data.accessMode | quote }}] accessModes: [{{ .Values.registry.volumes.data.accessMode | quote }}]
{{- if .Values.registry.volumes.data.storageClass }} {{- if .Values.registry.volumes.data.storageClass }}

View File

@ -3,10 +3,10 @@ kind: Service
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-registry" name: "{{ template "harbor.fullname" . }}-registry"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
spec: spec:
ports: ports:
- port: 5000 - port: 5000
selector: selector:
{{ include "helm.matchLabels" . | indent 4 }} {{ include "harbor.matchLabels" . | indent 4 }}
component: registry component: registry

View File

@ -3,7 +3,7 @@ kind: ConfigMap
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-ui" name: "{{ template "harbor.fullname" . }}-ui"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
data: data:
app.conf: |+ app.conf: |+
appname = Harbor appname = Harbor

View File

@ -3,14 +3,14 @@ kind: Deployment
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-ui" name: "{{ template "harbor.fullname" . }}-ui"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
component: ui component: ui
spec: spec:
replicas: 1 replicas: 1
template: template:
metadata: metadata:
labels: labels:
{{ include "helm.labels" . | indent 8 }} {{ include "harbor.matchLabels" . | indent 8 }}
component: ui component: ui
spec: spec:
containers: containers:
@ -64,7 +64,7 @@ spec:
secret: secret:
secretName: "{{ template "harbor.fullname" . }}-ui" secretName: "{{ template "harbor.fullname" . }}-ui"
items: items:
- key: key - key: secretKey
path: key path: key
- name: ui-secrets-private-key - name: ui-secrets-private-key
secret: secret:

View File

@ -3,10 +3,10 @@ kind: Secret
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-ui" name: "{{ template "harbor.fullname" . }}-ui"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
type: Opaque type: Opaque
data: data:
secretKey: {{ .Values.secretKey | b64enc | quote }}
secret: {{ .Values.ui.secret | b64enc | quote }} secret: {{ .Values.ui.secret | b64enc | quote }}
key: {{ .Values.ui.key | b64enc | quote }}
private_key.pem: {{ .Values.ui.privateKeyPem | b64enc | quote }} private_key.pem: {{ .Values.ui.privateKeyPem | b64enc | quote }}
jobserviceSecret: {{ .Values.jobservice.secret | b64enc | quote }} jobserviceSecret: {{ .Values.jobservice.secret | b64enc | quote }}

View File

@ -3,11 +3,11 @@ kind: Service
metadata: metadata:
name: "{{ template "harbor.fullname" . }}-ui" name: "{{ template "harbor.fullname" . }}-ui"
labels: labels:
{{ include "helm.labels" . | indent 4 }} {{ include "harbor.labels" . | indent 4 }}
spec: spec:
ports: ports:
- port: 80 - port: 80
targetPort: 8080 targetPort: 8080
selector: selector:
{{ include "helm.matchLabels" . | indent 4 }} {{ include "harbor.matchLabels" . | indent 4 }}
component: ui component: ui

View File

@ -1,5 +1,5 @@
# Configure persisten Volumes per application # Configure persisten Volumes per application
## Applications that require storage have a `volumes` defintion which will be used ## Applications that require storage have a `volumes` definition which will be used
## when `persistence.enabled` is set to true. ## when `persistence.enabled` is set to true.
## example ## example
# mysql: # mysql:
@ -28,81 +28,34 @@
persistence: persistence:
enabled: false enabled: false
externalDomain: harbor.192.168.99.100.xip.io # The tag for Harbor docker images.
## tls_crt, tls_key, ca_crt should match the domain above harborImageTag: &harbor_image_tag v1.4.0
tlsCrt: | # The FQDN for Harbor service.
-----BEGIN CERTIFICATE----- externalDomain: harbor.my.domain
MIIDJDCCAgygAwIBAgIJAKNSg1jp3l2oMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV # If set to true, you don't need to set tlsCrt/tlsKey/caCrt, but must add
BAMMB3Rlc3QtY2EwHhcNMTgwMTEzMTg1NTIwWhcNMTgwMzE0MTg1NTIwWjAnMSUw # Harbor FQDN as insecure-registries for your docker client.
IwYDVQQDDBxoYXJib3IuMTkyLjE2OC45OS4xMDAueGlwLmlvMIIBIjANBgkqhkiG insecureRegistry: false
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxlAD8tlryoGsweXCwDgfyPGkaM9hXsVLW2PH # The TLS certificate for Harbor. The common name of tlsCrt must match the externalDomain above.
/vGWBVMXOdxpFhuvH7tXmqN3Ek39YQjcsb+nHAGx7ynx6KFtvzcXCjGfeI1yuoN0 tlsCrt:
8H2sfV7yxtkVLu/uJGb8mSfsw9ubOR/zMbrsD1oH0tzi3cnW0kcbY0u0Xp/5g0PP tlsKey:
+tig0X+PDfumK/W6KnTOAmnfNTJwhhlljako+lveT5EjVtQMdJmV16PZJwCDA4b9 caCrt:
2U8EkLOjXcSg2ad03XxASGUuG8oMLHNXF0zcJ9421DviaRQGJUSjR571t/YCc2KK
AQVZ/zSI5duQVysfMZrjiuvSQfKSWRVY6z0JAWH7+Dx+1u8ilwIDAQABo2gwZjAJ # The secret key used for encryption. Must be a string of 16 chars.
BgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYB secretKey: not-a-secure-key
BQUHAwEwLQYDVR0RBCYwJIIcaGFyYm9yLjE5Mi4xNjguOTkuMTAwLnhpcC5pb4cE
wKhjZDANBgkqhkiG9w0BAQsFAAOCAQEATgS0Y2wQiCQrVfiDFSIxtIBK2af0qtoA # These annotations allow the registry to work behind the nginx
J4DZ/1Jo01uGycFCyt9KOKbmFubrJu9NHuACL9od3RI37k6L73lV2zB3sS4NEcH2 # ingress controller.
SvF+rOE7gmtgJULHCDFEWSMxHdUFwcdG1trRVe+9Gyp/LGdC4yyycmwquz7YXf+r ingress:
7b5r26rFAYmO8rWYtDt4clC3JSR3O1BmF5ktRNzUtRvrzr3UuwYz0Wy72S/Sa+Iu annotations:
RnassP8mg6PCppeGccYFcFihL9kDl4g4Xu/PaMiKdxjdeAV6xAd7VbKBZSi/ljnF ingress.kubernetes.io/ssl-redirect: "true"
OUUUi7MDJuUWbHEb0XrEXNzihBzf7bu4I2MftQidIg6LwWjiYZRHmw== ingress.kubernetes.io/body-size: "0"
-----END CERTIFICATE----- ingress.kubernetes.io/proxy-body-size: "0"
tlsKey: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAxlAD8tlryoGsweXCwDgfyPGkaM9hXsVLW2PH/vGWBVMXOdxp
FhuvH7tXmqN3Ek39YQjcsb+nHAGx7ynx6KFtvzcXCjGfeI1yuoN08H2sfV7yxtkV
Lu/uJGb8mSfsw9ubOR/zMbrsD1oH0tzi3cnW0kcbY0u0Xp/5g0PP+tig0X+PDfum
K/W6KnTOAmnfNTJwhhlljako+lveT5EjVtQMdJmV16PZJwCDA4b92U8EkLOjXcSg
2ad03XxASGUuG8oMLHNXF0zcJ9421DviaRQGJUSjR571t/YCc2KKAQVZ/zSI5duQ
VysfMZrjiuvSQfKSWRVY6z0JAWH7+Dx+1u8ilwIDAQABAoIBAQC2QDDwzRm/2N6w
r3wEdU/YtyJWZEfi9cRkb9YMGW+64vrUZRh6heSyb9R5vEKgouX6eE+CV1S3a2Ng
HZdBKKIYegOFjcc13iCTAl7E6WpNKaZKUpSiN0QPVkpMYqG3+am0nQU+Lb/l9+J6
yh8Anw763vhvj9Jqp/CBzx9jNBTPkh6u02Ayhegn7BBIpxk3LmdWSFn4IBXSxnMs
6B9h8motQFXRJDFm37YFl3834jNWilJT2Z/MCumoAGwNhOFFd5wZM5St1jvfFQlw
A44+AbnOf9sArukXa2NA/HHs6hZHt9GN10kbMBj9wbQRN960OKK4P6+8vVrJ+gUu
iodHLiaxAoGBAP+hlsJvqatgLJmqrODpWrhRqXxWNYs3VJXR5XEEtygVMe2FT7a6
pu5GWgjpQUHFqgqSNpRiJnxdI+AELH6AkeTMg4EyCoaJJKaitMslnCvQHL5oQjIb
IjJrxk/EObxh/7NuSf/nzUBfmhJhZ/pz6LbBLqiy35Cpq106XVC/XSMJAoGBAMaZ
Qd2nUQPhR7+wxDT38duKRIendYd4BiGqc7z9M91+HLjNg9W7cRPx6bUCaN5E4uwx
PzixsHmWc5H1bDgf9ymAMvexfB3BTXfO9tRn6nZu+XbcN6eJejKYj2iVMjDZrVHu
FZzrusRwPXI1I+b8rnKvNF+wf9DQcIVl7VW1G2CfAoGBAMLPSizzG8JWkKaqwwTD
0TcWRKtUp4loqTVjuA7hIROS03HHXnBK3lxHkOWpnOma0XMs6hs6kUnFUUmu5Jmj
MYvDr5QNpqfQa/XxmQYXq2RYPQ9+NLQqqWzzZTX0vGsr48nCCvLSnECqmqfXQ35C
Rt6/aed2KZn9M3Lgv6yBqWDBAoGAIEXjeDuqZLEFUddN6zWnrf+IJ2tFJCCTDoF+
kWWsOgA2dqmfFOqC87TKP8oGdKhJIAzYs0Pc48VZPozdazl2lt3oamwDOWqiRifx
4I6KgXiDPZeHy8gBfZthIqOsJlgZXEkOZhPApA+BTL/p9610Q9rI7gvmmW5l+qeX
q+fkbQ0CgYBYwF4lcMrON5k8cwZMTyPMwOsY+TsxpsoqUqPPHm9JW2DZDQ1f7oEm
1b6zTkwtqbnQX7vKosEivCbeQLN0XOZms+BM/KIwcuZjKxy/rkMNaFcqWlOvktug
hk8Jkkt1dANV5rNPYEkt7G+PiL7ApOV6fGvJRA4f8+RFrfPFtku4Sw==
-----END RSA PRIVATE KEY-----
caCrt: |
-----BEGIN CERTIFICATE-----
MIIC9zCCAd+gAwIBAgIJAPRSQQK2Q7dsMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
BAMMB3Rlc3QtY2EwHhcNMTgwMTEzMTg1NTIwWhcNMjgwMTExMTg1NTIwWjASMRAw
DgYDVQQDDAd0ZXN0LWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
znp0w37dGXlUgAXx5p4cwQ/XEiZGA1NorbcV3RTox3wX4b0bFJwKij8hLFMRZrEd
f40AxvQnyTcoX80c0w1M+3fG/pq5PhsfjrphX/EZ/rYJDZkO4vz60H8uc2g9AgVR
IvYbMobX7KcRruyi2dnt22q6O6Xy0pCkTE/+UAgcbwUCNDA9H/+8RhmXkAEaIyc8
y5vIpwfjiSdX6Kqv5zg0ZRESE+s9g6+U4NfwHbeUqfl6/ZuP8xXy2az3tdTqN8l0
dCMjv/dpLzPAOaZhzj+BYN1iVMTFhm6FzszkdTuvJliCIUJeyIqvzqz+k+ai8xR9
s0hrZrTzmN2id5J5cWSWawIDAQABo1AwTjAdBgNVHQ4EFgQU1L/db3zQjJW8ycmd
3D3jh4/HtJEwHwYDVR0jBBgwFoAU1L/db3zQjJW8ycmd3D3jh4/HtJEwDAYDVR0T
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAJrNwGShGJhmVSoBsSi+4tEa98UC8
7ULKyvaK+RPYKb4K2igMFHZD3KhaayShEG3rw/Y53hdmU+3I82tQ+txGmQoqicxg
BwAODdvixH5gP4idh7G1Q6tDvgJGGl2HvcE8fzbVIO3qDKefPlif20eX2gUc/Ut5
gyiyJutOQKVjEUb5bmUaeRyTXo8Vf2TIhIRfdXHg2ueWj2lDWbtVxQbn/m7aSqON
9YN5xfXY36tpVp40RV1J36FUskkhgc/DZcgEMYdAr2XrjDS1A0TnEaDatQUgYgpd
J0oP9V+2FMfDFvIhX5tNrEuIIFMyO+HR0wxV7huTUeus4knyXBZur3If+g==
-----END CERTIFICATE-----
adminserver: adminserver:
image: image:
repository: vmware/harbor-adminserver repository: vmware/harbor-adminserver
tag: v1.3.0 tag: *harbor_image_tag
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
emailHost: "smtp.mydomain.com" emailHost: "smtp.mydomain.com"
emailPort: "25" emailPort: "25"
@ -110,7 +63,7 @@ adminserver:
emailSsl: "false" emailSsl: "false"
emailFrom: "admin <sample_admin@mydomain.com>" emailFrom: "admin <sample_admin@mydomain.com>"
emailIdentity: "" emailIdentity: ""
key: not-a-secure-key emailInsecure: "False"
emailPwd: not-a-secure-password emailPwd: not-a-secure-password
harborAdminPassword: Harbor12345 harborAdminPassword: Harbor12345
## Persist data to a persistent volume ## Persist data to a persistent volume
@ -129,9 +82,8 @@ adminserver:
jobservice: jobservice:
image: image:
repository: vmware/harbor-jobservice repository: vmware/harbor-jobservice
tag: v1.3.0 tag: *harbor_image_tag
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
key: not-a-secure-key
secret: not-a-secure-secret secret: not-a-secure-secret
# resources: # resources:
# requests: # requests:
@ -143,10 +95,9 @@ jobservice:
ui: ui:
image: image:
repository: vmware/harbor-ui repository: vmware/harbor-ui
tag: v1.3.0 tag: *harbor_image_tag
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
secret: not-a-secure-secret secret: not-a-secure-secret
key: not-a-secure-key
privateKeyPem: | privateKeyPem: |
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
MIIJKAIBAAKCAgEA4WYbxdrFGG6RnfyYKlHYML3lEqtA9cYWWOynE9BeaEr/cMnM MIIJKAIBAAKCAgEA4WYbxdrFGG6RnfyYKlHYML3lEqtA9cYWWOynE9BeaEr/cMnM
@ -211,7 +162,7 @@ ui:
mysql: mysql:
image: image:
repository: vmware/harbor-db repository: vmware/harbor-db
tag: v1.3.0 tag: *harbor_image_tag
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
# If left blank will use the included mysql service name. # If left blank will use the included mysql service name.
host: ~ host: ~
@ -231,8 +182,8 @@ mysql:
registry: registry:
image: image:
repository: registry repository: vmware/registry-photon
tag: "2.6.2" tag: v2.6.2-v1.4.0
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
httpSecret: not-a-secure-secret httpSecret: not-a-secure-secret
logLevel: logLevel:
@ -296,12 +247,12 @@ registry:
## Enabling it will just break things. ## Enabling it will just break things.
# #
clair: clair:
enabled: false enabled: true
postgresPassword: not-a-secure-password
image: image:
repository: vmware/clair repository: vmware/clair-photon
tag: v2.0.1-photon tag: v2.0.1-v1.4.0
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
postgresPassword: not-a-secure-password
pgImage: pgImage:
repository: postgres repository: postgres
tag: "9.6.4" tag: "9.6.4"

View File

@ -995,6 +995,86 @@ paths:
description: Forbidden. description: Forbidden.
'404': '404':
description: Repository not found. description: Repository not found.
'/repositories/{repo_name}/labels':
get:
summary: Get labels of a repository.
description: |
Get labels of a repository specified by the repo_name.
parameters:
- name: repo_name
in: path
type: string
required: true
description: The name of repository.
tags:
- Products
responses:
'200':
description: Successfully.
schema:
type: array
items:
$ref: '#/definitions/Label'
'401':
description: Unauthorized.
'403':
description: Forbidden. User should have read permisson for the repository to perform the action.
'404':
description: Repository not found.
post:
summary: Add a label to the repository.
description: |
Add a label to the repository.
parameters:
- name: repo_name
in: path
type: string
required: true
description: The name of repository.
- name: label
in: body
description: Only the ID property is required.
required: true
schema:
$ref: '#/definitions/Label'
tags:
- Products
responses:
'200':
description: Successfully.
'401':
description: Unauthorized.
'403':
description: Forbidden. User should have write permisson for the repository to perform the action.
'404':
description: Resource not found.
'/repositories/{repo_name}/labels/{label_id}':
delete:
summary: Delete label from the repository.
description: |
Delete the label from the repository specified by the repo_name.
parameters:
- name: repo_name
in: path
type: string
required: true
description: The name of repository.
- name: label_id
in: path
type: integer
required: true
description: The ID of label.
tags:
- Products
responses:
'200':
description: Successfully.
'401':
description: Unauthorized.
'403':
description: Forbidden. User should have write permisson for the repository to perform the action.
'404':
description: Resource not found.
'/repositories/{repo_name}/tags/{tag}': '/repositories/{repo_name}/tags/{tag}':
get: get:
summary: Get the tag of the repository. summary: Get the tag of the repository.
@ -1075,6 +1155,101 @@ paths:
$ref: '#/definitions/DetailedTag' $ref: '#/definitions/DetailedTag'
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
'/repositories/{repo_name}/tags/{tag}/labels':
get:
summary: Get labels of an image.
description: |
Get labels of an image specified by the repo_name and tag.
parameters:
- name: repo_name
in: path
type: string
required: true
description: The name of repository.
- name: tag
in: path
type: string
required: true
description: The tag of the image.
tags:
- Products
responses:
'200':
description: Successfully.
schema:
type: array
items:
$ref: '#/definitions/Label'
'401':
description: Unauthorized.
'403':
description: Forbidden. User should have read permisson for the image to perform the action.
'404':
description: Resource not found.
post:
summary: Add a label to image.
description: |
Add a label to the image.
parameters:
- name: repo_name
in: path
type: string
required: true
description: The name of repository.
- name: tag
in: path
type: string
required: true
description: The tag of the image.
- name: label
in: body
description: Only the ID property is required.
required: true
schema:
$ref: '#/definitions/Label'
tags:
- Products
responses:
'200':
description: Successfully.
'401':
description: Unauthorized.
'403':
description: Forbidden. User should have write permisson for the image to perform the action.
'404':
description: Resource not found.
'/repositories/{repo_name}/tags/{tag}/labels/{label_id}':
delete:
summary: Delete label from the image.
description: |
Delete the label from the image specified by the repo_name and tag.
parameters:
- name: repo_name
in: path
type: string
required: true
description: The name of repository.
- name: tag
in: path
type: string
required: true
description: The tag of the image.
- name: label_id
in: path
type: integer
required: true
description: The ID of label.
tags:
- Products
responses:
'200':
description: Successfully.
'401':
description: Unauthorized.
'403':
description: Forbidden. User should have write permisson for the image to perform the action.
'404':
description: Resource not found.
'/repositories/{repo_name}/tags/{tag}/manifest': '/repositories/{repo_name}/tags/{tag}/manifest':
get: get:
summary: Get manifests of a relevant repository. summary: Get manifests of a relevant repository.
@ -1639,6 +1814,164 @@ paths:
project and target. project and target.
'500': '500':
description: Unexpected internal errors. description: Unexpected internal errors.
/labels:
get:
summary: List labels according to the query strings.
description: >
This endpoint let user list labels by name, scope and project_id
parameters:
- name: name
in: query
type: string
required: false
description: The label name.
- name: scope
in: query
type: string
required: true
description: The label scope. Valid values are g and p. g for global labels and p for project labels.
- name: project_id
in: query
type: integer
format: int64
required: false
description: Relevant project ID, required when scope is p.
- name: page
in: query
type: integer
format: int32
required: false
description: The page nubmer.
- name: page_size
in: query
type: integer
format: int32
required: false
description: The size of per page.
tags:
- Products
responses:
'200':
description: Get successfully.
schema:
type: array
items:
$ref: '#/definitions/Label'
'400':
description: Invalid parameters.
'401':
description: User need to log in first.
'500':
description: Unexpected internal errors.
post:
summary: Post creates a label
description: >
This endpoint let user creates a label.
parameters:
- name: label
in: body
description: The json object of label.
required: true
schema:
$ref: '#/definitions/Label'
tags:
- Products
responses:
'201':
description: Create successfully.
'400':
description: Invalid parameters.
'401':
description: User need to log in first.
'409':
description: >-
Label with the same name and same scope already exists.
'415':
$ref: '#/responses/UnsupportedMediaType'
'500':
description: Unexpected internal errors.
'/labels/{id}':
get:
summary: Get the label specified by ID.
description: |
This endpoint let user get the label by specific ID.
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Label ID
tags:
- Products
responses:
'200':
description: Get successfully.
schema:
$ref: '#/definitions/Label'
'401':
description: User need to log in first.
'404':
description: The resource does not exist.
'500':
description: Unexpected internal errors.
put:
summary: Update the label properties.
description: >
This endpoint let user update label properties.
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Label ID
- name: label
in: body
description: The updated label json object.
required: true
schema:
$ref: '#/definitions/Label'
tags:
- Products
responses:
'200':
description: Update successfully.
'400':
description: Invalid parameters.
'401':
description: User need to log in first.
'404':
description: The resource does not exist.
'409':
description: >-
The label with the same name already exists.
'500':
description: Unexpected internal errors.
delete:
summary: Delete the label specified by ID.
description: >
Delete the label specified by ID.
parameters:
- name: id
in: path
type: integer
format: int64
required: true
description: Label ID
tags:
- Products
responses:
'200':
description: Delete successfully.
'400':
description: Invalid parameters.
'401':
description: User need to log in first.
'404':
description: The resource does not exist.
'500':
description: Unexpected internal errors.
/replications: /replications:
post: post:
summary: Trigger the replication according to the specified policy. summary: Trigger the replication according to the specified policy.
@ -2881,6 +3214,11 @@ definitions:
type: array type: array
items: items:
$ref: '#/definitions/ComponentOverviewEntry' $ref: '#/definitions/ComponentOverviewEntry'
labels:
type: array
description: The label list.
items:
$ref: '#/definitions/Label'
ComponentOverviewEntry: ComponentOverviewEntry:
type: object type: object
properties: properties:
@ -2914,6 +3252,11 @@ definitions:
tags_count: tags_count:
type: integer type: integer
description: The tags count of repository. description: The tags count of repository.
labels:
type: array
description: The label list.
items:
$ref: '#/definitions/Label'
creation_time: creation_time:
type: string type: string
description: The creation time of repository. description: The creation time of repository.
@ -3056,3 +3399,30 @@ definitions:
status: status:
type: string type: string
description: The status of jobs. The only valid value is stop for now. description: The status of jobs. The only valid value is stop for now.
Label:
type: object
properties:
id:
type: integer
description: The ID of label.
name:
type: string
description: The name of label.
description:
type: string
description: The description of label.
color:
type: string
description: The color of label.
scope:
type: integer
description: The scope of label, g for global labels and p for project labels.
project_id:
type: integer
description: The project ID if the label is a project label.
creation_time:
type: string
description: The creation time of label.
update_time:
type: string
description: The update time of label.

View File

@ -12,6 +12,10 @@ LDAP_UID=$ldap_uid
LDAP_SCOPE=$ldap_scope LDAP_SCOPE=$ldap_scope
LDAP_TIMEOUT=$ldap_timeout LDAP_TIMEOUT=$ldap_timeout
LDAP_VERIFY_CERT=$ldap_verify_cert LDAP_VERIFY_CERT=$ldap_verify_cert
LDAP_GROUP_BASEDN=$ldap_group_basedn
LDAP_GROUP_FILTER=$ldap_group_filter
LDAP_GROUP_GID=$ldap_group_gid
LDAP_GROUP_SCOPE=$ldap_group_scope
DATABASE_TYPE=mysql DATABASE_TYPE=mysql
MYSQL_HOST=$db_host MYSQL_HOST=$db_host
MYSQL_PORT=$db_port MYSQL_PORT=$db_port

View File

@ -91,6 +91,18 @@ ldap_timeout = 5
#Verify certificate from LDAP server #Verify certificate from LDAP server
ldap_verify_cert = true ldap_verify_cert = true
#The base dn from which to lookup a group in LDAP/AD
ldap_group_basedn = ou=group,dc=mydomain,dc=com
#filter to search LDAP/AD group
ldap_group_filter = objectclass=group
#The attribute used to name a LDAP/AD group, it could be cn, name
ldap_group_gid = cn
#The scope to search for ldap groups. 0-LDAP_SCOPE_BASE, 1-LDAP_SCOPE_ONELEVEL, 2-LDAP_SCOPE_SUBTREE
ldap_group_scope = 2
#Turn on or off the self-registration feature #Turn on or off the self-registration feature
self_registration = on self_registration = on

View File

@ -254,6 +254,41 @@ create table properties (
UNIQUE (k) UNIQUE (k)
); );
create table harbor_label (
id int NOT NULL AUTO_INCREMENT,
name varchar(128) NOT NULL,
description text,
color varchar(16),
# 's' for system level labels
# 'u' for user level labels
level char(1) NOT NULL,
# 'g' for global labels
# 'p' for project labels
scope char(1) NOT NULL,
project_id int,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
PRIMARY KEY(id),
CONSTRAINT unique_name_and_scope UNIQUE (name,scope)
);
create table harbor_resource_label (
id int NOT NULL AUTO_INCREMENT,
label_id int NOT NULL,
# the resource_id is the ID of project when the resource_type is p
# the resource_id is the ID of repository when the resource_type is r
# the resource_id is the name of image when the resource_type is i
resource_id varchar(256) NOT NULL,
# 'p' for project
# 'r' for repository
# 'i' for image
resource_type char(1) NOT NULL,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
PRIMARY KEY(id),
CONSTRAINT unique_label_resource UNIQUE (label_id,resource_id, resource_type)
);
CREATE TABLE IF NOT EXISTS `alembic_version` ( CREATE TABLE IF NOT EXISTS `alembic_version` (
`version_num` varchar(32) NOT NULL `version_num` varchar(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -111,7 +111,7 @@ create table project_metadata (
creation_time timestamp, creation_time timestamp,
update_time timestamp, update_time timestamp,
deleted tinyint (1) DEFAULT 0 NOT NULL, deleted tinyint (1) DEFAULT 0 NOT NULL,
UNIQUE(project_id, name) ON CONFLICT REPLACE, UNIQUE(project_id, name),
FOREIGN KEY (project_id) REFERENCES project(project_id) FOREIGN KEY (project_id) REFERENCES project(project_id)
); );
@ -240,6 +240,47 @@ create table properties (
UNIQUE(k) UNIQUE(k)
); );
create table harbor_label (
id INTEGER PRIMARY KEY,
name varchar(128) NOT NULL,
description text,
color varchar(16),
/*
's' for system level labels
'u' for user level labels
*/
level char(1) NOT NULL,
/*
'g' for global labels
'p' for project labels
*/
scope char(1) NOT NULL,
project_id int,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP,
UNIQUE(name, scope)
);
create table harbor_resource_label (
id INTEGER PRIMARY KEY,
label_id int NOT NULL,
/*
the resource_id is the ID of project when the resource_type is p
the resource_id is the ID of repository when the resource_type is r
the resource_id is the name of image when the resource_type is i
*/
resource_id varchar(256) NOT NULL,
/*
'p' for project
'r' for repository
'i' for image
*/
resource_type char(1) NOT NULL,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP,
UNIQUE (label_id,resource_id, resource_type)
);
create table alembic_version ( create table alembic_version (
version_num varchar(32) NOT NULL version_num varchar(32) NOT NULL
); );

View File

@ -224,6 +224,10 @@ ldap_uid = rcp.get("configuration", "ldap_uid")
ldap_scope = rcp.get("configuration", "ldap_scope") ldap_scope = rcp.get("configuration", "ldap_scope")
ldap_timeout = rcp.get("configuration", "ldap_timeout") ldap_timeout = rcp.get("configuration", "ldap_timeout")
ldap_verify_cert = rcp.get("configuration", "ldap_verify_cert") ldap_verify_cert = rcp.get("configuration", "ldap_verify_cert")
ldap_group_basedn = rcp.get("configuration", "ldap_group_basedn")
ldap_group_filter = rcp.get("configuration", "ldap_group_filter")
ldap_group_gid = rcp.get("configuration", "ldap_group_gid")
ldap_group_scope = rcp.get("configuration", "ldap_group_scope")
db_password = rcp.get("configuration", "db_password") db_password = rcp.get("configuration", "db_password")
db_host = rcp.get("configuration", "db_host") db_host = rcp.get("configuration", "db_host")
db_user = rcp.get("configuration", "db_user") db_user = rcp.get("configuration", "db_user")
@ -325,6 +329,10 @@ render(os.path.join(templates_dir, "adminserver", "env"),
ldap_scope=ldap_scope, ldap_scope=ldap_scope,
ldap_verify_cert=ldap_verify_cert, ldap_verify_cert=ldap_verify_cert,
ldap_timeout=ldap_timeout, ldap_timeout=ldap_timeout,
ldap_group_basedn=ldap_group_basedn,
ldap_group_filter=ldap_group_filter,
ldap_group_gid=ldap_group_gid,
ldap_group_scope=ldap_group_scope,
db_password=db_password, db_password=db_password,
db_host=db_host, db_host=db_host,
db_user=db_user, db_user=db_user,

View File

@ -0,0 +1,14 @@
package api
import (
"net/http"
"github.com/vmware/harbor/src/common/utils/log"
)
// Ping monitor the server status
func Ping(w http.ResponseWriter, r *http.Request) {
if err := writeJSON(w, "Pong"); err != nil {
log.Errorf("Failed to write response: %v", err)
return
}
}

View File

@ -0,0 +1,16 @@
package api
import(
"testing"
"net/http/httptest"
"net/http"
"github.com/stretchr/testify/assert"
"io/ioutil"
)
func TestPing(t *testing.T) {
w := httptest.NewRecorder()
Ping(w, nil)
assert.Equal(t, http.StatusOK, w.Code)
result, _:= ioutil.ReadAll(w.Body)
assert.Equal(t, "\"Pong\"", string(result))
}

View File

@ -31,7 +31,10 @@ func NewHandler() http.Handler {
"uiSecret": os.Getenv("UI_SECRET"), "uiSecret": os.Getenv("UI_SECRET"),
"jobserviceSecret": os.Getenv("JOBSERVICE_SECRET"), "jobserviceSecret": os.Getenv("JOBSERVICE_SECRET"),
} }
h = newAuthHandler(auth.NewSecretAuthenticator(secrets), h) insecureAPIs := map[string]bool{
"/api/ping":true,
}
h = newAuthHandler(auth.NewSecretAuthenticator(secrets), h, insecureAPIs)
h = gorilla_handlers.LoggingHandler(os.Stdout, h) h = gorilla_handlers.LoggingHandler(os.Stdout, h)
return h return h
} }
@ -39,12 +42,14 @@ func NewHandler() http.Handler {
type authHandler struct { type authHandler struct {
authenticator auth.Authenticator authenticator auth.Authenticator
handler http.Handler handler http.Handler
insecureAPIs map[string]bool
} }
func newAuthHandler(authenticator auth.Authenticator, handler http.Handler) http.Handler { func newAuthHandler(authenticator auth.Authenticator, handler http.Handler, insecureAPIs map[string]bool) http.Handler {
return &authHandler{ return &authHandler{
authenticator: authenticator, authenticator: authenticator,
handler: handler, handler: handler,
insecureAPIs: insecureAPIs,
} }
} }
@ -56,6 +61,12 @@ func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
if a.insecureAPIs !=nil && a.insecureAPIs[r.URL.Path] {
if a.handler != nil {
a.handler.ServeHTTP(w, r)
}
return
}
valid, err := a.authenticator.Authenticate(r) valid, err := a.authenticator.Authenticate(r)
if err != nil { if err != nil {
log.Errorf("failed to authenticate request: %v", err) log.Errorf("failed to authenticate request: %v", err)

View File

@ -45,28 +45,40 @@ func TestNewAuthHandler(t *testing.T) {
cases := []struct { cases := []struct {
authenticator auth.Authenticator authenticator auth.Authenticator
handler http.Handler handler http.Handler
insecureAPIs map[string]bool
responseCode int responseCode int
requestURL string
}{ }{
{nil, nil, http.StatusOK}, {nil, nil, nil, http.StatusOK,"http://localhost/good"},
{&fakeAuthenticator{ {&fakeAuthenticator{
authenticated: false, authenticated: false,
err: nil, err: nil,
}, nil, http.StatusUnauthorized}, }, nil, nil, http.StatusUnauthorized,"http://localhost/hello"},
{&fakeAuthenticator{ {&fakeAuthenticator{
authenticated: false, authenticated: false,
err: errors.New("error"), err: errors.New("error"),
}, nil, http.StatusInternalServerError}, }, nil, nil, http.StatusInternalServerError,"http://localhost/hello"},
{&fakeAuthenticator{ {&fakeAuthenticator{
authenticated: true, authenticated: true,
err: nil, err: nil,
}, &fakeHandler{http.StatusNotFound}, http.StatusNotFound}, }, &fakeHandler{http.StatusNotFound}, nil, http.StatusNotFound,"http://localhost/notexsit"},
{&fakeAuthenticator{
authenticated: false,
err: nil,
}, &fakeHandler{http.StatusOK},map[string]bool{"/api/ping":true,},http.StatusOK,"http://localhost/api/ping"},
} }
for _, c := range cases { for _, c := range cases {
handler := newAuthHandler(c.authenticator, c.handler) handler := newAuthHandler(c.authenticator, c.handler, c.insecureAPIs)
w := httptest.NewRecorder() w := httptest.NewRecorder()
handler.ServeHTTP(w, nil) r := httptest.NewRequest("GET",c.requestURL,nil)
handler.ServeHTTP(w, r)
assert.Equal(t, c.responseCode, w.Code, "unexpected response code") assert.Equal(t, c.responseCode, w.Code, "unexpected response code")
} }
handler := NewHandler()
w := httptest.NewRecorder()
r := httptest.NewRequest("GET","http://localhost/api/ping",nil)
handler.ServeHTTP(w,r)
} }

View File

@ -27,5 +27,6 @@ func newRouter() http.Handler {
r.HandleFunc("/api/configurations", api.UpdateCfgs).Methods("PUT") r.HandleFunc("/api/configurations", api.UpdateCfgs).Methods("PUT")
r.HandleFunc("/api/configurations/reset", api.ResetCfgs).Methods("POST") r.HandleFunc("/api/configurations/reset", api.ResetCfgs).Methods("POST")
r.HandleFunc("/api/systeminfo/capacity", api.Capacity).Methods("GET") r.HandleFunc("/api/systeminfo/capacity", api.Capacity).Methods("GET")
r.HandleFunc("/api/ping", api.Ping).Methods("GET")
return r return r
} }

View File

@ -89,6 +89,13 @@ var (
env: "LDAP_VERIFY_CERT", env: "LDAP_VERIFY_CERT",
parse: parseStringToBool, parse: parseStringToBool,
}, },
common.LDAPGroupBaseDN: "LDAP_GROUP_BASEDN",
common.LDAPGroupSearchFilter: "LDAP_GROUP_FILTER",
common.LDAPGroupAttributeName: "LDAP_GROUP_GID",
common.LDAPGroupSearchScope: &parser{
env: "LDAP_GROUP_SCOPE",
parse: parseStringToInt,
},
common.EmailHost: "EMAIL_HOST", common.EmailHost: "EMAIL_HOST",
common.EmailPort: &parser{ common.EmailPort: &parser{
env: "EMAIL_PORT", env: "EMAIL_PORT",
@ -152,6 +159,13 @@ var (
repeatLoadEnvs = map[string]interface{}{ repeatLoadEnvs = map[string]interface{}{
common.ExtEndpoint: "EXT_ENDPOINT", common.ExtEndpoint: "EXT_ENDPOINT",
common.MySQLPassword: "MYSQL_PWD", common.MySQLPassword: "MYSQL_PWD",
common.MySQLHost: "MYSQL_HOST",
common.MySQLUsername: "MYSQL_USR",
common.MySQLDatabase: "MYSQL_DATABASE",
common.MySQLPort: &parser{
env: "MYSQL_PORT",
parse: parseStringToInt,
},
common.MaxJobWorkers: &parser{ common.MaxJobWorkers: &parser{
env: "MAX_JOB_WORKERS", env: "MAX_JOB_WORKERS",
parse: parseStringToInt, parse: parseStringToInt,
@ -170,6 +184,12 @@ var (
parse: parseStringToBool, parse: parseStringToBool,
}, },
common.ClairDBPassword: "CLAIR_DB_PASSWORD", common.ClairDBPassword: "CLAIR_DB_PASSWORD",
common.ClairDBHost: "CLAIR_DB_HOST",
common.ClairDBUsername: "CLAIR_DB_USERNAME",
common.ClairDBPort: &parser{
env: "CLAIR_DB_PORT",
parse: parseStringToInt,
},
common.UAAEndpoint: "UAA_ENDPOINT", common.UAAEndpoint: "UAA_ENDPOINT",
common.UAAClientID: "UAA_CLIENTID", common.UAAClientID: "UAA_CLIENTID",
common.UAAClientSecret: "UAA_CLIENTSECRET", common.UAAClientSecret: "UAA_CLIENTSECRET",
@ -382,4 +402,5 @@ func validLdapScope(cfg map[string]interface{}, isMigrate bool) {
ldapScope = 0 ldapScope = 0
} }
cfg[ldapScopeKey] = ldapScope cfg[ldapScopeKey] = ldapScope
} }

View File

@ -29,6 +29,15 @@ const (
RoleDeveloper = 2 RoleDeveloper = 2
RoleGuest = 3 RoleGuest = 3
LabelLevelSystem = "s"
LabelLevelUser = "u"
LabelScopeGlobal = "g"
LabelScopeProject = "p"
ResourceTypeProject = "p"
ResourceTypeRepository = "r"
ResourceTypeImage = "i"
ExtEndpoint = "ext_endpoint" ExtEndpoint = "ext_endpoint"
AUTHMode = "auth_mode" AUTHMode = "auth_mode"
DatabaseType = "database_type" DatabaseType = "database_type"
@ -50,6 +59,10 @@ const (
LDAPScope = "ldap_scope" LDAPScope = "ldap_scope"
LDAPTimeout = "ldap_timeout" LDAPTimeout = "ldap_timeout"
LDAPVerifyCert = "ldap_verify_cert" LDAPVerifyCert = "ldap_verify_cert"
LDAPGroupBaseDN = "ldap_group_base_dn"
LDAPGroupSearchFilter = "ldap_group_search_filter"
LDAPGroupAttributeName = "ldap_group_attribute_name"
LDAPGroupSearchScope = "ldap_group_search_scope"
TokenServiceURL = "token_service_url" TokenServiceURL = "token_service_url"
RegistryURL = "registry_url" RegistryURL = "registry_url"
EmailHost = "email_host" EmailHost = "email_host"

99
src/common/dao/label.go Normal file
View File

@ -0,0 +1,99 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"time"
"github.com/astaxie/beego/orm"
"github.com/vmware/harbor/src/common/models"
)
// AddLabel creates a label
func AddLabel(label *models.Label) (int64, error) {
now := time.Now()
label.CreationTime = now
label.UpdateTime = now
return GetOrmer().Insert(label)
}
// GetLabel specified by ID
func GetLabel(id int64) (*models.Label, error) {
label := &models.Label{
ID: id,
}
if err := GetOrmer().Read(label); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return label, nil
}
// GetTotalOfLabels returns the total count of labels
func GetTotalOfLabels(query *models.LabelQuery) (int64, error) {
qs := getLabelQuerySetter(query)
return qs.Count()
}
// ListLabels list labels according to the query conditions
func ListLabels(query *models.LabelQuery) ([]*models.Label, error) {
qs := getLabelQuerySetter(query)
if query.Size > 0 {
qs = qs.Limit(query.Size)
if query.Page > 0 {
qs = qs.Offset((query.Page - 1) * query.Size)
}
}
qs = qs.OrderBy("Name")
labels := []*models.Label{}
_, err := qs.All(&labels)
return labels, err
}
func getLabelQuerySetter(query *models.LabelQuery) orm.QuerySeter {
qs := GetOrmer().QueryTable(&models.Label{})
if len(query.Name) > 0 {
qs = qs.Filter("Name", query.Name)
}
if len(query.Level) > 0 {
qs = qs.Filter("Level", query.Level)
}
if len(query.Scope) > 0 {
qs = qs.Filter("Scope", query.Scope)
}
if query.ProjectID != 0 {
qs = qs.Filter("ProjectID", query.ProjectID)
}
return qs
}
// UpdateLabel ...
func UpdateLabel(label *models.Label) error {
label.UpdateTime = time.Now()
_, err := GetOrmer().Update(label)
return err
}
// DeleteLabel ...
func DeleteLabel(id int64) error {
_, err := GetOrmer().Delete(&models.Label{
ID: id,
})
return err
}

View File

@ -0,0 +1,91 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"testing"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMethodsOfLabel(t *testing.T) {
label := &models.Label{
Name: "test",
Level: common.LabelLevelUser,
Scope: common.LabelScopeProject,
ProjectID: 1,
}
// add
id, err := AddLabel(label)
require.Nil(t, err)
label.ID = id
// get
l, err := GetLabel(id)
require.Nil(t, err)
assert.Equal(t, label.ID, l.ID)
assert.Equal(t, label.Name, l.Name)
assert.Equal(t, label.Scope, l.Scope)
assert.Equal(t, label.ProjectID, l.ProjectID)
// get total count
total, err := GetTotalOfLabels(&models.LabelQuery{
Scope: common.LabelScopeProject,
ProjectID: 1,
})
require.Nil(t, err)
assert.Equal(t, int64(1), total)
// list
labels, err := ListLabels(&models.LabelQuery{
Scope: common.LabelScopeProject,
ProjectID: 1,
Name: label.Name,
})
require.Nil(t, err)
assert.Equal(t, 1, len(labels))
// list
labels, err = ListLabels(&models.LabelQuery{
Scope: common.LabelScopeProject,
ProjectID: 1,
Name: "not_exist_label",
})
require.Nil(t, err)
assert.Equal(t, 0, len(labels))
// update
newName := "dev"
label.Name = newName
err = UpdateLabel(label)
require.Nil(t, err)
l, err = GetLabel(id)
require.Nil(t, err)
assert.Equal(t, newName, l.Name)
// delete
err = DeleteLabel(id)
require.Nil(t, err)
l, err = GetLabel(id)
require.Nil(t, err)
assert.Nil(t, l)
}

View File

@ -0,0 +1,74 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"time"
"github.com/astaxie/beego/orm"
"github.com/vmware/harbor/src/common/models"
)
// AddResourceLabel add a label to a resource
func AddResourceLabel(rl *models.ResourceLabel) (int64, error) {
now := time.Now()
rl.CreationTime = now
rl.UpdateTime = now
return GetOrmer().Insert(rl)
}
// GetResourceLabel specified by ID
func GetResourceLabel(rType, rID string, labelID int64) (*models.ResourceLabel, error) {
rl := &models.ResourceLabel{
ResourceType: rType,
ResourceID: rID,
LabelID: labelID,
}
if err := GetOrmer().Read(rl, "ResourceType", "ResourceID", "LabelID"); err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
return nil, err
}
return rl, nil
}
// GetLabelsOfResource returns the label list of the resource
func GetLabelsOfResource(rType, rID string) ([]*models.Label, error) {
sql := `select l.id, l.name, l.description, l.color, l.scope, l.project_id, l.creation_time, l.update_time
from harbor_resource_label rl
join harbor_label l on rl.label_id=l.id
where rl.resource_type = ? and rl.resource_id = ?`
labels := []*models.Label{}
_, err := GetOrmer().Raw(sql, rType, rID).QueryRows(&labels)
return labels, err
}
// DeleteResourceLabel ...
func DeleteResourceLabel(id int64) error {
_, err := GetOrmer().Delete(&models.ResourceLabel{
ID: id,
})
return err
}
// DeleteLabelsOfResource removes all labels of resource specified by rType and rID
func DeleteLabelsOfResource(rType, rID string) error {
_, err := GetOrmer().QueryTable(&models.ResourceLabel{}).
Filter("ResourceType", rType).
Filter("ResourceID", rID).Delete()
return err
}

View File

@ -0,0 +1,74 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/models"
)
func TestMethodsOfResourceLabel(t *testing.T) {
labelID, err := AddLabel(&models.Label{
Name: "test_label",
Level: common.LabelLevelUser,
Scope: common.LabelScopeGlobal,
})
require.Nil(t, err)
defer DeleteLabel(labelID)
resourceID := "1"
resourceType := common.ResourceTypeRepository
// add
rl := &models.ResourceLabel{
LabelID: labelID,
ResourceType: resourceType,
ResourceID: resourceID,
}
id, err := AddResourceLabel(rl)
require.Nil(t, err)
// get
r, err := GetResourceLabel(resourceType, resourceID, labelID)
require.Nil(t, err)
assert.Equal(t, id, r.ID)
// get by resource
labels, err := GetLabelsOfResource(resourceType, resourceID)
require.Nil(t, err)
require.Equal(t, 1, len(labels))
assert.Equal(t, id, r.ID)
// delete
err = DeleteResourceLabel(id)
require.Nil(t, err)
labels, err = GetLabelsOfResource(resourceType, resourceID)
require.Nil(t, err)
require.Equal(t, 0, len(labels))
// delete by resource
id, err = AddResourceLabel(rl)
require.Nil(t, err)
err = DeleteLabelsOfResource(resourceType, resourceID)
require.Nil(t, err)
labels, err = GetLabelsOfResource(resourceType, resourceID)
require.Nil(t, err)
require.Equal(t, 0, len(labels))
}

View File

@ -32,5 +32,7 @@ func init() {
new(ClairVulnTimestamp), new(ClairVulnTimestamp),
new(WatchItem), new(WatchItem),
new(ProjectMetadata), new(ProjectMetadata),
new(ConfigEntry)) new(ConfigEntry),
new(Label),
new(ResourceLabel))
} }

View File

@ -0,0 +1,81 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"fmt"
"time"
"github.com/astaxie/beego/validation"
"github.com/vmware/harbor/src/common"
)
// Label holds information used for a label
type Label struct {
ID int64 `orm:"pk;auto;column(id)" json:"id"`
Name string `orm:"column(name)" json:"name"`
Description string `orm:"column(description)" json:"description"`
Color string `orm:"column(color)" json:"color"`
Level string `orm:"column(level)" json:"-"`
Scope string `orm:"column(scope)" json:"scope"`
ProjectID int64 `orm:"column(project_id)" json:"project_id"`
CreationTime time.Time `orm:"column(creation_time)" json:"creation_time"`
UpdateTime time.Time `orm:"column(update_time)" json:"update_time"`
}
// TableName ...
func (l *Label) TableName() string {
return "harbor_label"
}
// LabelQuery : query parameters for labels
type LabelQuery struct {
Name string
Level string
Scope string
ProjectID int64
Pagination
}
// Valid ...
func (l *Label) Valid(v *validation.Validation) {
if len(l.Name) == 0 {
v.SetError("name", "cannot be empty")
}
if len(l.Name) > 128 {
v.SetError("name", "max length is 128")
}
if l.Scope != common.LabelScopeGlobal && l.Scope != common.LabelScopeProject {
v.SetError("scope", fmt.Sprintf("invalid: %s", l.Scope))
} else if l.Scope == common.LabelScopeProject && l.ProjectID <= 0 {
v.SetError("project_id", fmt.Sprintf("invalid: %d", l.ProjectID))
}
}
// ResourceLabel records the relationship between resource and label
type ResourceLabel struct {
ID int64 `orm:"pk;auto;column(id)"`
LabelID int64 `orm:"column(label_id)"`
ResourceID string `orm:"column(resource_id)"`
ResourceType string `orm:"column(resource_type)"`
CreationTime time.Time `orm:"column(creation_time)"`
UpdateTime time.Time `orm:"column(update_time)"`
}
// TableName ...
func (r *ResourceLabel) TableName() string {
return "harbor_resource_label"
}

View File

@ -0,0 +1,86 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"testing"
"github.com/astaxie/beego/validation"
"github.com/stretchr/testify/assert"
)
func TestValidOfLabel(t *testing.T) {
cases := []struct {
label *Label
hasError bool
}{
{
label: &Label{
Name: "",
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "",
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "invalid_scope",
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "g",
},
hasError: false,
},
{
label: &Label{
Name: "test",
Scope: "p",
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "p",
ProjectID: -1,
},
hasError: true,
},
{
label: &Label{
Name: "test",
Scope: "p",
ProjectID: 1,
},
hasError: false,
},
}
for _, c := range cases {
v := &validation.Validation{}
c.label.Valid(v)
assert.Equal(t, c.hasError, v.HasErrors())
}
}

View File

@ -27,12 +27,21 @@ type LdapConf struct {
LdapVerifyCert bool `json:"ldap_verify_cert"` LdapVerifyCert bool `json:"ldap_verify_cert"`
} }
// LdapGroupConf holds information about ldap group
type LdapGroupConf struct {
LdapGroupBaseDN string `json:"ldap_group_base_dn,omitempty"`
LdapGroupFilter string `json:"ldap_group_filter,omitempty"`
LdapGroupNameAttribute string `json:"ldap_group_name_attribute,omitempty"`
LdapGroupSearchScope int `json:"ldap_group_search_scope"`
}
// LdapUser ... // LdapUser ...
type LdapUser struct { type LdapUser struct {
Username string `json:"ldap_username"` Username string `json:"ldap_username"`
Email string `json:"ldap_email"` Email string `json:"ldap_email"`
Realname string `json:"ldap_realname"` Realname string `json:"ldap_realname"`
DN string `json:"-"` DN string `json:"-"`
GroupDNList []string `json:"ldap_groupdn"`
} }
//LdapImportUser ... //LdapImportUser ...
@ -45,3 +54,9 @@ type LdapFailedImportUser struct {
UID string `json:"uid"` UID string `json:"uid"`
Error string `json:"err_msg"` Error string `json:"err_msg"`
} }
// LdapGroup ...
type LdapGroup struct {
GroupName string `json:"group_name,omitempty"`
GroupDN string `json:"group_dn,omitempty"`
}

View File

@ -95,3 +95,8 @@ func transformVuln(clairVuln *models.ClairLayerEnvelope) (*models.ComponentsOver
Summary: compSummary, Summary: compSummary,
}, overallSev }, overallSev
} }
//TransformVuln is for running scanning job in both job service V1 and V2.
func TransformVuln(clairVuln *models.ClairLayerEnvelope) (*models.ComponentsOverview, models.Severity) {
return transformVuln(clairVuln)
}

View File

@ -130,6 +130,7 @@ func newClient(addr, identity, username, password string,
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
tls = true
} else { } else {
log.Debugf("the email server %s does not support STARTTLS", addr) log.Debugf("the email server %s does not support STARTTLS", addr)
} }
@ -137,9 +138,13 @@ func newClient(addr, identity, username, password string,
if ok, _ := client.Extension("AUTH"); ok { if ok, _ := client.Extension("AUTH"); ok {
log.Debug("authenticating the client...") log.Debug("authenticating the client...")
// only support plain auth var auth smtp.Auth
if err = client.Auth(smtp.PlainAuth(identity, if tls {
username, password, host)); err != nil { auth = smtp.PlainAuth(identity, username, password, host)
} else {
auth = smtp.CRAMMD5Auth(username, password)
}
if err = client.Auth(auth); err != nil {
return nil, err return nil, err
} }
} else { } else {

View File

@ -185,6 +185,7 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) {
for _, ldapEntry := range result.Entries { for _, ldapEntry := range result.Entries {
var u models.LdapUser var u models.LdapUser
groupDNList := []string{}
for _, attr := range ldapEntry.Attributes { for _, attr := range ldapEntry.Attributes {
//OpenLdap sometimes contain leading space in useranme //OpenLdap sometimes contain leading space in useranme
val := strings.TrimSpace(attr.Values[0]) val := strings.TrimSpace(attr.Values[0])
@ -200,7 +201,10 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) {
u.Email = val u.Email = val
case "email": case "email":
u.Email = val u.Email = val
case "memberof":
groupDNList = append(groupDNList, val)
} }
u.GroupDNList = groupDNList
} }
u.DN = ldapEntry.DN u.DN = ldapEntry.DN
ldapUsers = append(ldapUsers, u) ldapUsers = append(ldapUsers, u)
@ -248,20 +252,28 @@ func (session *Session) Open() error {
// SearchLdap to search ldap with the provide filter // SearchLdap to search ldap with the provide filter
func (session *Session) SearchLdap(filter string) (*goldap.SearchResult, error) { func (session *Session) SearchLdap(filter string) (*goldap.SearchResult, error) {
attributes := []string{"uid", "cn", "mail", "email", "memberof"}
if err := session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword); err != nil {
return nil, fmt.Errorf("Can not bind search dn, error: %v", err)
}
attributes := []string{"uid", "cn", "mail", "email"}
lowerUID := strings.ToLower(session.ldapConfig.LdapUID) lowerUID := strings.ToLower(session.ldapConfig.LdapUID)
if lowerUID != "uid" && lowerUID != "cn" && lowerUID != "mail" && lowerUID != "email" { if lowerUID != "uid" && lowerUID != "cn" && lowerUID != "mail" && lowerUID != "email" {
attributes = append(attributes, session.ldapConfig.LdapUID) attributes = append(attributes, session.ldapConfig.LdapUID)
} }
return session.SearchLdapAttribute(session.ldapConfig.LdapBaseDn, filter, attributes)
}
// SearchLdapAttribute - to search ldap with the provide filter, with specified attributes
func (session *Session) SearchLdapAttribute(baseDN, filter string, attributes []string) (*goldap.SearchResult, error) {
if err := session.Bind(session.ldapConfig.LdapSearchDn, session.ldapConfig.LdapSearchPassword); err != nil {
return nil, fmt.Errorf("Can not bind search dn, error: %v", err)
}
filter = strings.TrimSpace(filter)
if !(strings.HasPrefix(filter, "(") || strings.HasSuffix(filter, ")")) {
filter = "(" + filter + ")"
}
log.Debugf("Search ldap with filter:%v", filter) log.Debugf("Search ldap with filter:%v", filter)
searchRequest := goldap.NewSearchRequest( searchRequest := goldap.NewSearchRequest(
session.ldapConfig.LdapBaseDn, baseDN,
session.ldapConfig.LdapScope, session.ldapConfig.LdapScope,
goldap.NeverDerefAliases, goldap.NeverDerefAliases,
0, //Unlimited results 0, //Unlimited results
@ -318,3 +330,69 @@ func (session *Session) Close() {
session.ldapConn.Close() session.ldapConn.Close()
} }
} }
//SearchGroupByName ...
func (session *Session) SearchGroupByName(groupName string) ([]models.LdapGroup, error) {
ldapGroupConfig, err := config.LDAPGroupConf()
log.Debugf("Ldap group config: %+v", ldapGroupConfig)
if err != nil {
return nil, err
}
return session.searchGroup(ldapGroupConfig.LdapGroupBaseDN, ldapGroupConfig.LdapGroupFilter, groupName, ldapGroupConfig.LdapGroupNameAttribute)
}
//SearchGroupByDN ...
func (session *Session) SearchGroupByDN(groupDN string) ([]models.LdapGroup, error) {
ldapGroupConfig, err := config.LDAPGroupConf()
log.Debugf("Ldap group config: %+v", ldapGroupConfig)
if err != nil {
return nil, err
}
return session.searchGroup(groupDN, ldapGroupConfig.LdapGroupFilter, "", ldapGroupConfig.LdapGroupNameAttribute)
}
func (session *Session) searchGroup(baseDN, filter, groupName, groupNameAttribute string) ([]models.LdapGroup, error) {
ldapGroups := make([]models.LdapGroup, 0)
log.Debugf("Groupname: %v, basedn: %v", groupName, baseDN)
ldapFilter := createGroupSearchFilter(filter, groupName, groupNameAttribute)
attributes := []string{groupNameAttribute}
result, err := session.SearchLdapAttribute(baseDN, ldapFilter, attributes)
if err != nil {
return nil, err
}
for _, ldapEntry := range result.Entries {
var group models.LdapGroup
group.GroupDN = ldapEntry.DN
for _, attr := range ldapEntry.Attributes {
//OpenLdap sometimes contain leading space in useranme
val := strings.TrimSpace(attr.Values[0])
log.Debugf("Current ldap entry attr name: %s\n", attr.Name)
switch strings.ToLower(attr.Name) {
case strings.ToLower(groupNameAttribute):
group.GroupName = val
}
}
ldapGroups = append(ldapGroups, group)
}
return ldapGroups, nil
}
func createGroupSearchFilter(oldFilter, groupName, groupNameAttribute string) string {
filter := ""
groupName = goldap.EscapeFilter(groupName)
groupNameAttribute = goldap.EscapeFilter(groupNameAttribute)
if len(oldFilter) == 0 {
if len(groupName) == 0 {
filter = groupNameAttribute + "=*"
} else {
filter = groupNameAttribute + "=*" + groupName + "*"
}
} else {
if len(groupName) == 0 {
filter = oldFilter
} else {
filter = "(&(" + oldFilter + ")(" + groupNameAttribute + "=*" + groupName + "*))"
}
}
return filter
}

View File

@ -15,15 +15,16 @@ package ldap
import ( import (
"os" "os"
"reflect"
"testing" "testing"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/common/utils/test" "github.com/vmware/harbor/src/common/utils/test"
uiConfig "github.com/vmware/harbor/src/ui/config" uiConfig "github.com/vmware/harbor/src/ui/config"
goldap "gopkg.in/ldap.v2"
) )
var adminServerLdapTestConfig = map[string]interface{}{ var adminServerLdapTestConfig = map[string]interface{}{
@ -217,6 +218,14 @@ func TestSearchUser(t *testing.T) {
t.Fatalf("failed to search user test!") t.Fatalf("failed to search user test!")
} }
result2, err := session.SearchUser("mike")
if err != nil || len(result2) == 0 {
t.Fatalf("failed to search user mike!")
}
if len(result2[0].GroupDNList) < 1 && result2[0].GroupDNList[0] != "cn=harbor_users,ou=groups,dc=example,dc=com" {
t.Fatalf("failed to search user mike's memberof")
}
} }
func TestFormatURL(t *testing.T) { func TestFormatURL(t *testing.T) {
@ -254,3 +263,80 @@ func TestFormatURL(t *testing.T) {
} }
} }
func Test_createGroupSearchFilter(t *testing.T) {
type args struct {
oldFilter string
groupName string
groupNameAttribute string
}
tests := []struct {
name string
args args
want string
}{
{"Normal Filter", args{oldFilter: "objectclass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"}, "(&(objectclass=groupOfNames)(cn=*harbor_users*))"},
{"Empty Old", args{groupName: "harbor_users", groupNameAttribute: "cn"}, "cn=*harbor_users*"},
{"Empty Both", args{groupNameAttribute: "cn"}, "cn=*"},
{"Empty name", args{oldFilter: "objectclass=groupOfNames", groupNameAttribute: "cn"}, "objectclass=groupOfNames"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := createGroupSearchFilter(tt.args.oldFilter, tt.args.groupName, tt.args.groupNameAttribute); got != tt.want {
t.Errorf("createGroupSearchFilter() = %v, want %v", got, tt.want)
}
})
}
}
func TestSession_SearchGroup(t *testing.T) {
type fields struct {
ldapConfig models.LdapConf
ldapConn *goldap.Conn
}
type args struct {
baseDN string
filter string
groupName string
groupNameAttribute string
}
ldapConfig := models.LdapConf{
LdapURL: adminServerLdapTestConfig[common.LDAPURL].(string) + ":389",
LdapSearchDn: adminServerLdapTestConfig[common.LDAPSearchDN].(string),
LdapScope: 2,
LdapSearchPassword: adminServerLdapTestConfig[common.LDAPSearchPwd].(string),
LdapBaseDn: adminServerLdapTestConfig[common.LDAPBaseDN].(string),
}
tests := []struct {
name string
fields fields
args args
want []models.LdapGroup
wantErr bool
}{
{"normal search",
fields{ldapConfig: ldapConfig},
args{baseDN: "dc=example,dc=com", filter: "objectClass=groupOfNames", groupName: "harbor_users", groupNameAttribute: "cn"},
[]models.LdapGroup{models.LdapGroup{GroupName: "harbor_users", GroupDN: "cn=harbor_users,ou=groups,dc=example,dc=com"}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
session := &Session{
ldapConfig: tt.fields.ldapConfig,
ldapConn: tt.fields.ldapConn,
}
session.Open()
defer session.Close()
got, err := session.searchGroup(tt.args.baseDN, tt.args.filter, tt.args.groupName, tt.args.groupNameAttribute)
if (err != nil) != tt.wantErr {
t.Errorf("Session.SearchGroup() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Session.SearchGroup() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -42,6 +42,10 @@ var adminServerDefaultConfig = map[string]interface{}{
common.LDAPFilter: "", common.LDAPFilter: "",
common.LDAPScope: 3, common.LDAPScope: 3,
common.LDAPTimeout: 30, common.LDAPTimeout: 30,
common.LDAPGroupBaseDN: "dc=example,dc=com",
common.LDAPGroupSearchFilter: "objectClass=groupOfNames",
common.LDAPGroupSearchScope: 2,
common.LDAPGroupAttributeName: "cn",
common.TokenServiceURL: "http://token_service", common.TokenServiceURL: "http://token_service",
common.RegistryURL: "http://registry", common.RegistryURL: "http://registry",
common.EmailHost: "127.0.0.1", common.EmailHost: "127.0.0.1",

View File

@ -0,0 +1,139 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package scan
import (
"crypto/sha256"
"encoding/json"
"fmt"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/schema2"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/job"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/clair"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice_v2/env"
"github.com/vmware/harbor/src/jobservice_v2/job/impl/utils"
)
// ClairJob is the struct to scan Harbor's Image with Clair
type ClairJob struct {
}
// MaxFails implements the interface in job/Interface
func (cj *ClairJob) MaxFails() uint {
return 1
}
// ShouldRetry implements the interface in job/Interface
func (cj *ClairJob) ShouldRetry() bool {
return false
}
// Validate implements the interface in job/Interface
func (cj *ClairJob) Validate(params map[string]interface{}) error {
return nil
}
// Run implements the interface in job/Interface
func (cj *ClairJob) Run(ctx env.JobContext, params map[string]interface{}) error {
// TODO: get logger from ctx?
logger := log.DefaultLogger()
jobParms, err := transformParam(params)
if err != nil {
logger.Errorf("Failed to prepare parms for scan job, error: %v", err)
return err
}
repoClient, err := utils.NewRepositoryClientForJobservice(jobParms.Repository, jobParms.RegistryURL, jobParms.Secret, jobParms.TokenEndpoint)
if err != nil {
return err
}
imgDigest, _, payload, err := repoClient.PullManifest(jobParms.Tag, []string{schema2.MediaTypeManifest})
if err != nil {
logger.Errorf("Error pulling manifest for image %s:%s :%v", jobParms.Repository, jobParms.Tag, err)
return err
}
token, err := utils.GetTokenForRepo(jobParms.Repository, jobParms.Secret, jobParms.TokenEndpoint)
if err != nil {
logger.Errorf("Failed to get token, error: %v", err)
return err
}
layers, err := prepareLayers(payload, jobParms.RegistryURL, jobParms.Repository, token)
if err != nil {
logger.Errorf("Failed to prepare layers, error: %v", err)
return err
}
clairClient := clair.NewClient(jobParms.ClairEndpoint, logger)
for _, l := range layers {
logger.Infof("Scanning Layer: %s, path: %s", l.Name, l.Path)
if err := clairClient.ScanLayer(l); err != nil {
logger.Errorf("Failed to scan layer: %s, error: %v", l.Name, err)
return err
}
}
layerName := layers[len(layers)-1].Name
res, err := clairClient.GetResult(layerName)
if err != nil {
logger.Errorf("Failed to get result from Clair, error: %v", err)
return err
}
compOverview, sev := clair.TransformVuln(res)
err = dao.UpdateImgScanOverview(imgDigest, layerName, sev, compOverview)
return err
}
func transformParam(params map[string]interface{}) (*job.ScanJobParms, error) {
res := job.ScanJobParms{}
parmsBytes, err := json.Marshal(params)
if err != nil {
return nil, err
}
err = json.Unmarshal(parmsBytes, &res)
return &res, err
}
func prepareLayers(payload []byte, registryURL, repo, tk string) ([]models.ClairLayer, error) {
layers := []models.ClairLayer{}
manifest, _, err := distribution.UnmarshalManifest(schema2.MediaTypeManifest, payload)
if err != nil {
return layers, err
}
tokenHeader := map[string]string{"Connection": "close", "Authorization": fmt.Sprintf("Bearer %s", tk)}
// form the chain by using the digests of all parent layers in the image, such that if another image is built on top of this image the layer name can be re-used.
shaChain := ""
for _, d := range manifest.References() {
if d.MediaType == schema2.MediaTypeConfig {
continue
}
shaChain += string(d.Digest) + "-"
l := models.ClairLayer{
Name: fmt.Sprintf("%x", sha256.Sum256([]byte(shaChain))),
Headers: tokenHeader,
Format: "Docker",
Path: utils.BuildBlobURL(registryURL, repo, string(d.Digest)),
}
if len(layers) > 0 {
l.ParentName = layers[len(layers)-1].Name
}
layers = append(layers, l)
}
return layers, nil
}

View File

@ -0,0 +1,85 @@
package utils
import (
"fmt"
"net/http"
"github.com/docker/distribution/registry/auth/token"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/registry"
"github.com/vmware/harbor/src/common/utils/registry/auth"
)
// NewRepositoryClient creates a repository client with standard token authorizer
func NewRepositoryClient(endpoint string, insecure bool, credential auth.Credential,
tokenServiceEndpoint, repository string) (*registry.Repository, error) {
transport := registry.GetHTTPTransport(insecure)
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
Transport: transport,
}, credential, tokenServiceEndpoint)
uam := &userAgentModifier{
userAgent: "harbor-registry-client",
}
return registry.NewRepository(repository, endpoint, &http.Client{
Transport: registry.NewTransport(transport, authorizer, uam),
})
}
// NewRepositoryClientForJobservice creates a repository client that can only be used to
// access the internal registry
func NewRepositoryClientForJobservice(repository, internalRegistryURL, secret, internalTokenServiceURL string) (*registry.Repository, error) {
transport := registry.GetHTTPTransport()
credential := auth.NewCookieCredential(&http.Cookie{
Name: models.UISecretCookie,
Value: secret,
})
authorizer := auth.NewStandardTokenAuthorizer(&http.Client{
Transport: transport,
}, credential, internalTokenServiceURL)
uam := &userAgentModifier{
userAgent: "harbor-registry-client",
}
return registry.NewRepository(repository, internalRegistryURL, &http.Client{
Transport: registry.NewTransport(transport, authorizer, uam),
})
}
type userAgentModifier struct {
userAgent string
}
// Modify adds user-agent header to the request
func (u *userAgentModifier) Modify(req *http.Request) error {
req.Header.Set(http.CanonicalHeaderKey("User-Agent"), u.userAgent)
return nil
}
// BuildBlobURL ...
func BuildBlobURL(endpoint, repository, digest string) string {
return fmt.Sprintf("%s/v2/%s/blobs/%s", endpoint, repository, digest)
}
//GetTokenForRepo is used for job handler to get a token for clair.
func GetTokenForRepo(repository, secret, internalTokenServiceURL string) (string, error) {
c := &http.Cookie{Name: models.UISecretCookie, Value: secret}
credentail := auth.NewCookieCredential(c)
t, err := auth.GetToken(internalTokenServiceURL, true, credentail,
[]*token.ResourceActions{&token.ResourceActions{
Type: "repository",
Name: repository,
Actions: []string{"pull"},
}})
if err != nil {
return "", err
}
return t.Token, nil
}

View File

@ -10,12 +10,14 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/vmware/harbor/src/common/job"
"github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/jobservice_v2/api" "github.com/vmware/harbor/src/jobservice_v2/api"
"github.com/vmware/harbor/src/jobservice_v2/config" "github.com/vmware/harbor/src/jobservice_v2/config"
"github.com/vmware/harbor/src/jobservice_v2/core" "github.com/vmware/harbor/src/jobservice_v2/core"
"github.com/vmware/harbor/src/jobservice_v2/env" "github.com/vmware/harbor/src/jobservice_v2/env"
"github.com/vmware/harbor/src/jobservice_v2/job/impl" "github.com/vmware/harbor/src/jobservice_v2/job/impl"
"github.com/vmware/harbor/src/jobservice_v2/job/impl/scan"
"github.com/vmware/harbor/src/jobservice_v2/pool" "github.com/vmware/harbor/src/jobservice_v2/pool"
) )
@ -141,6 +143,11 @@ func (bs *Bootstrap) loadAndRunRedisWorkerPool(ctx *env.Context, cfg config.Conf
ctx.ErrorChan <- err ctx.ErrorChan <- err
return redisWorkerPool //avoid nil pointer issue return redisWorkerPool //avoid nil pointer issue
} }
if err := redisWorkerPool.RegisterJob(job.ImageScanJob, (*scan.ClairJob)(nil)); err != nil {
//exit
ctx.ErrorChan <- err
return redisWorkerPool //avoid nil pointer issue
}
redisWorkerPool.Start() redisWorkerPool.Start()

View File

@ -41,6 +41,10 @@ var (
common.LDAPScope, common.LDAPScope,
common.LDAPTimeout, common.LDAPTimeout,
common.LDAPVerifyCert, common.LDAPVerifyCert,
common.LDAPGroupAttributeName,
common.LDAPGroupBaseDN,
common.LDAPGroupSearchFilter,
common.LDAPGroupSearchScope,
common.EmailHost, common.EmailHost,
common.EmailPort, common.EmailPort,
common.EmailUsername, common.EmailUsername,
@ -66,6 +70,9 @@ var (
common.LDAPBaseDN, common.LDAPBaseDN,
common.LDAPUID, common.LDAPUID,
common.LDAPFilter, common.LDAPFilter,
common.LDAPGroupAttributeName,
common.LDAPGroupBaseDN,
common.LDAPGroupSearchFilter,
common.EmailHost, common.EmailHost,
common.EmailUsername, common.EmailUsername,
common.EmailPassword, common.EmailPassword,
@ -80,6 +87,7 @@ var (
common.EmailPort, common.EmailPort,
common.LDAPScope, common.LDAPScope,
common.LDAPTimeout, common.LDAPTimeout,
common.LDAPGroupSearchScope,
common.TokenExpiration, common.TokenExpiration,
} }

View File

@ -111,6 +111,10 @@ func init() {
beego.Router("/api/users/?:id", &UserAPI{}) beego.Router("/api/users/?:id", &UserAPI{})
beego.Router("/api/logs", &LogAPI{}) beego.Router("/api/logs", &LogAPI{})
beego.Router("/api/repositories/*", &RepositoryAPI{}, "put:Put") beego.Router("/api/repositories/*", &RepositoryAPI{}, "put:Put")
beego.Router("/api/repositories/*/labels", &RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository")
beego.Router("/api/repositories/*/labels/:id([0-9]+", &RepositoryLabelAPI{}, "delete:RemoveFromRepository")
beego.Router("/api/repositories/*/tags/:tag/labels", &RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage")
beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+", &RepositoryLabelAPI{}, "delete:RemoveFromImage")
beego.Router("/api/repositories/*/tags/:tag", &RepositoryAPI{}, "delete:Delete;get:GetTag") beego.Router("/api/repositories/*/tags/:tag", &RepositoryAPI{}, "delete:Delete;get:GetTag")
beego.Router("/api/repositories/*/tags", &RepositoryAPI{}, "get:GetTags") beego.Router("/api/repositories/*/tags", &RepositoryAPI{}, "get:GetTags")
beego.Router("/api/repositories/*/tags/:tag/manifest", &RepositoryAPI{}, "get:GetManifests") beego.Router("/api/repositories/*/tags/:tag/manifest", &RepositoryAPI{}, "get:GetManifests")
@ -132,7 +136,9 @@ func init() {
beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset") beego.Router("/api/configurations/reset", &ConfigAPI{}, "post:Reset")
beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping") beego.Router("/api/email/ping", &EmailAPI{}, "post:Ping")
beego.Router("/api/replications", &ReplicationAPI{}) beego.Router("/api/replications", &ReplicationAPI{})
beego.Router("/api/labels", &LabelAPI{}, "post:Post;get:List")
beego.Router("/api/labels/:id([0-9]+", &LabelAPI{}, "get:Get;put:Put;delete:Delete")
beego.Router("/api/ping", &SystemInfoAPI{}, "get:Ping")
_ = updateInitPassword(1, "Harbor12345") _ = updateInitPassword(1, "Harbor12345")
if err := core.Init(); err != nil { if err := core.Init(); err != nil {
@ -985,6 +991,11 @@ func (a testapi) GetGeneralInfo() (int, []byte, error) {
return request(_sling, jsonAcceptHeader) return request(_sling, jsonAcceptHeader)
} }
func (a testapi) Ping() (int, []byte, error) {
_sling := sling.New().Get(a.basePath).Path("/api/ping")
return request(_sling, jsonAcceptHeader)
}
//Get system cert //Get system cert
func (a testapi) CertGet(authInfo usrInfo) (int, []byte, error) { func (a testapi) CertGet(authInfo usrInfo) (int, []byte, error) {
_sling := sling.New().Get(a.basePath) _sling := sling.New().Get(a.basePath)

263
src/ui/api/label.go Normal file
View File

@ -0,0 +1,263 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"strconv"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
)
// LabelAPI handles requests for label management
type LabelAPI struct {
label *models.Label
BaseController
}
// Prepare ...
func (l *LabelAPI) Prepare() {
l.BaseController.Prepare()
method := l.Ctx.Request.Method
if method == http.MethodGet {
return
}
// POST, PUT, DELETE need login first
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
}
if method == http.MethodPut || method == http.MethodDelete {
id, err := l.GetInt64FromPath(":id")
if err != nil || id <= 0 {
l.HandleBadRequest("invalid label ID")
return
}
label, err := dao.GetLabel(id)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", id, err))
return
}
if label == nil {
l.HandleNotFound(fmt.Sprintf("label %d not found", id))
return
}
if label.Scope == common.LabelScopeGlobal && !l.SecurityCtx.IsSysAdmin() ||
label.Scope == common.LabelScopeProject && !l.SecurityCtx.HasAllPerm(label.ProjectID) {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
l.label = label
}
}
// Post creates a label
func (l *LabelAPI) Post() {
label := &models.Label{}
l.DecodeJSONReqAndValidate(label)
label.Level = common.LabelLevelUser
switch label.Scope {
case common.LabelScopeGlobal:
if !l.SecurityCtx.IsSysAdmin() {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
label.ProjectID = 0
case common.LabelScopeProject:
exist, err := l.ProjectMgr.Exists(label.ProjectID)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %d: %v",
label.ProjectID, err))
return
}
if !exist {
l.HandleNotFound(fmt.Sprintf("project %d not found", label.ProjectID))
return
}
if !l.SecurityCtx.HasAllPerm(label.ProjectID) {
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
}
labels, err := dao.ListLabels(&models.LabelQuery{
Name: label.Name,
Level: label.Level,
Scope: label.Scope,
ProjectID: label.ProjectID,
})
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err))
return
}
if len(labels) > 0 {
l.HandleConflict()
return
}
id, err := dao.AddLabel(label)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to create label: %v", err))
return
}
l.Redirect(http.StatusCreated, strconv.FormatInt(id, 10))
}
// Get the label specified by ID
func (l *LabelAPI) Get() {
id, err := l.GetInt64FromPath(":id")
if err != nil || id <= 0 {
l.HandleBadRequest(fmt.Sprintf("invalid label ID: %s", l.GetStringFromPath(":id")))
return
}
label, err := dao.GetLabel(id)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", id, err))
return
}
if label == nil {
l.HandleNotFound(fmt.Sprintf("label %d not found", id))
return
}
if label.Scope == common.LabelScopeProject {
if !l.SecurityCtx.HasReadPerm(label.ProjectID) {
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
}
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
}
l.Data["json"] = label
l.ServeJSON()
}
// List labels according to the query strings
func (l *LabelAPI) List() {
query := &models.LabelQuery{
Name: l.GetString("name"),
Level: common.LabelLevelUser,
}
scope := l.GetString("scope")
if scope != common.LabelScopeGlobal && scope != common.LabelScopeProject {
l.HandleBadRequest(fmt.Sprintf("invalid scope: %s", scope))
return
}
query.Scope = scope
if scope == common.LabelScopeProject {
projectIDStr := l.GetString("project_id")
if len(projectIDStr) == 0 {
l.HandleBadRequest("project_id is required")
return
}
projectID, err := strconv.ParseInt(projectIDStr, 10, 64)
if err != nil || projectID <= 0 {
l.HandleBadRequest(fmt.Sprintf("invalid project_id: %s", projectIDStr))
return
}
if !l.SecurityCtx.HasReadPerm(projectID) {
if !l.SecurityCtx.IsAuthenticated() {
l.HandleUnauthorized()
return
}
l.HandleForbidden(l.SecurityCtx.GetUsername())
return
}
query.ProjectID = projectID
}
total, err := dao.GetTotalOfLabels(query)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to get total count of labels: %v", err))
return
}
query.Page, query.Size = l.GetPaginationParams()
labels, err := dao.ListLabels(query)
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err))
return
}
l.SetPaginationHeader(total, query.Page, query.Size)
l.Data["json"] = labels
l.ServeJSON()
}
// Put updates the label
func (l *LabelAPI) Put() {
label := &models.Label{}
l.DecodeJSONReq(label)
oldName := l.label.Name
// only name, description and color can be changed
l.label.Name = label.Name
l.label.Description = label.Description
l.label.Color = label.Color
l.Validate(l.label)
if l.label.Name != oldName {
labels, err := dao.ListLabels(&models.LabelQuery{
Name: l.label.Name,
Level: l.label.Level,
Scope: l.label.Scope,
ProjectID: l.label.ProjectID,
})
if err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to list labels: %v", err))
return
}
if len(labels) > 0 {
l.HandleConflict()
return
}
}
if err := dao.UpdateLabel(l.label); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to update label %d: %v", l.label.ID, err))
return
}
}
// Delete the label
func (l *LabelAPI) Delete() {
id := l.label.ID
if err := dao.DeleteLabel(id); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to delete label %d: %v", id, err))
return
}
}

435
src/ui/api/label_test.go Normal file
View File

@ -0,0 +1,435 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/models"
)
var (
labelAPIBasePath = "/api/labels"
labelID int64
)
func TestLabelAPIPost(t *testing.T) {
postFunc := func(resp *httptest.ResponseRecorder) error {
id, err := parseResourceID(resp)
if err != nil {
return err
}
labelID = id
return nil
}
cases := []*codeCheckingCase{
// 401
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
},
code: http.StatusUnauthorized,
},
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{},
credential: nonSysAdmin,
},
code: http.StatusBadRequest,
},
// 403 non-sysadmin try to create global label
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeGlobal,
},
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 non-member user try to create project label
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer try to create project label
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 404 non-exist project
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 10000,
},
credential: projAdmin,
},
code: http.StatusNotFound,
},
// 200
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projAdmin,
},
code: http.StatusCreated,
postFunc: postFunc,
},
// 409
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPost,
url: labelAPIBasePath,
bodyJSON: &models.Label{
Name: "test",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projAdmin,
},
code: http.StatusConflict,
},
}
runCodeCheckingCases(t, cases...)
}
func TestLabelAPIGet(t *testing.T) {
cases := []*codeCheckingCase{
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 0),
},
code: http.StatusBadRequest,
},
// 404
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 1000),
},
code: http.StatusNotFound,
},
// 200
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestLabelAPIList(t *testing.T) {
cases := []*codeCheckingCase{
// 400 no scope query string
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
},
code: http.StatusBadRequest,
},
// 400 invalid scope
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
queryStruct: struct {
Scope string `url:"scope"`
}{
Scope: "invalid_scope",
},
},
code: http.StatusBadRequest,
},
// 400 invalid project_id
&codeCheckingCase{
request: &testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
queryStruct: struct {
Scope string `url:"scope"`
ProjectID int64 `url:"project_id"`
}{
Scope: "p",
ProjectID: 0,
},
},
code: http.StatusBadRequest,
},
}
runCodeCheckingCases(t, cases...)
// 200
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
queryStruct: struct {
Scope string `url:"scope"`
ProjectID int64 `url:"project_id"`
Name string `url:"name"`
}{
Scope: "p",
ProjectID: 1,
Name: "test",
},
}, &labels)
require.Nil(t, err)
assert.Equal(t, 1, len(labels))
err = handleAndParse(&testingRequest{
method: http.MethodGet,
url: labelAPIBasePath,
queryStruct: struct {
Scope string `url:"scope"`
ProjectID int64 `url:"project_id"`
Name string `url:"name"`
}{
Scope: "p",
ProjectID: 1,
Name: "dev",
},
}, &labels)
require.Nil(t, err)
assert.Equal(t, 0, len(labels))
}
func TestLabelAPIPut(t *testing.T) {
cases := []*codeCheckingCase{
// 401
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
},
code: http.StatusUnauthorized,
},
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 0),
credential: nonSysAdmin,
},
code: http.StatusBadRequest,
},
// 404
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 10000),
credential: nonSysAdmin,
},
code: http.StatusNotFound,
},
// 403 non-member user
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
bodyJSON: &models.Label{
Name: "",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projAdmin,
},
code: http.StatusBadRequest,
},
// 200
&codeCheckingCase{
request: &testingRequest{
method: http.MethodPut,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
bodyJSON: &models.Label{
Name: "product",
Scope: common.LabelScopeProject,
ProjectID: 1,
},
credential: projAdmin,
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
label := &models.Label{}
err := handleAndParse(&testingRequest{
method: http.MethodGet,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
}, label)
require.Nil(t, err)
assert.Equal(t, "product", label.Name)
}
func TestLabelAPIDelete(t *testing.T) {
cases := []*codeCheckingCase{
// 401
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
},
code: http.StatusUnauthorized,
},
// 400
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 0),
credential: nonSysAdmin,
},
code: http.StatusBadRequest,
},
// 404
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, 10000),
credential: nonSysAdmin,
},
code: http.StatusNotFound,
},
// 403 non-member user
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: nonSysAdmin,
},
code: http.StatusForbidden,
},
// 403 developer
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: projDeveloper,
},
code: http.StatusForbidden,
},
// 200
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: projAdmin,
},
code: http.StatusOK,
},
// 404
&codeCheckingCase{
request: &testingRequest{
method: http.MethodDelete,
url: fmt.Sprintf("%s/%d", labelAPIBasePath, labelID),
credential: projAdmin,
},
code: http.StatusNotFound,
},
}
runCodeCheckingCases(t, cases...)
}

View File

@ -42,7 +42,7 @@ type ProjectAPI struct {
project *models.Project project *models.Project
} }
const projectNameMaxLen int = 30 const projectNameMaxLen int = 255
const projectNameMinLen int = 2 const projectNameMinLen int = 2
const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*` const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*`
@ -491,7 +491,7 @@ func (p *ProjectAPI) Logs() {
func validateProjectReq(req *models.ProjectRequest) error { func validateProjectReq(req *models.ProjectRequest) error {
pn := req.Name pn := req.Name
if isIllegalLength(req.Name, projectNameMinLen, projectNameMaxLen) { if isIllegalLength(req.Name, projectNameMinLen, projectNameMaxLen) {
return fmt.Errorf("Project name is illegal in length. (greater than 2 or less than 30)") return fmt.Errorf("Project name is illegal in length. (greater than %d or less than %d)", projectNameMaxLen, projectNameMinLen)
} }
validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`) validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`)
legal := validProjectName.MatchString(pn) legal := validProjectName.MatchString(pn)

View File

@ -25,6 +25,7 @@ import (
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/manifest/schema2"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models" "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/notifier" "github.com/vmware/harbor/src/common/notifier"
@ -54,6 +55,7 @@ type repoResp struct {
PullCount int64 `json:"pull_count"` PullCount int64 `json:"pull_count"`
StarCount int64 `json:"star_count"` StarCount int64 `json:"star_count"`
TagsCount int64 `json:"tags_count"` TagsCount int64 `json:"tags_count"`
Labels []*models.Label `json:"labels"`
CreationTime time.Time `json:"creation_time"` CreationTime time.Time `json:"creation_time"`
UpdateTime time.Time `json:"update_time"` UpdateTime time.Time `json:"update_time"`
} }
@ -78,6 +80,7 @@ type tagResp struct {
tagDetail tagDetail
Signature *notary.Target `json:"signature"` Signature *notary.Target `json:"signature"`
ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"` ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"`
Labels []*models.Label `json:"labels"`
} }
type manifestResp struct { type manifestResp struct {
@ -145,10 +148,10 @@ func getRepositories(projectID int64, keyword string,
return nil, err return nil, err
} }
return populateTagsCount(repositories) return assembleRepos(repositories)
} }
func populateTagsCount(repositories []*models.RepoRecord) ([]*repoResp, error) { func assembleRepos(repositories []*models.RepoRecord) ([]*repoResp, error) {
result := []*repoResp{} result := []*repoResp{}
for _, repository := range repositories { for _, repository := range repositories {
repo := &repoResp{ repo := &repoResp{
@ -167,6 +170,15 @@ func populateTagsCount(repositories []*models.RepoRecord) ([]*repoResp, error) {
return nil, err return nil, err
} }
repo.TagsCount = int64(len(tags)) repo.TagsCount = int64(len(tags))
labels, err := dao.GetLabelsOfResource(common.ResourceTypeRepository,
strconv.FormatInt(repository.RepositoryID, 10))
if err != nil {
log.Errorf("failed to get labels of repository %s: %v", repository.Name, err)
} else {
repo.Labels = labels
}
result = append(result, repo) result = append(result, repo)
} }
return result, nil return result, nil
@ -252,6 +264,11 @@ func (ra *RepositoryAPI) Delete() {
} }
for _, t := range tags { for _, t := range tags {
image := fmt.Sprintf("%s:%s", repoName, t)
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to delete labels of image %s: %v", image, err))
return
}
if err = rc.DeleteTag(t); err != nil { if err = rc.DeleteTag(t); err != nil {
if regErr, ok := err.(*registry_error.HTTPError); ok { if regErr, ok := err.(*registry_error.HTTPError); ok {
if regErr.StatusCode == http.StatusNotFound { if regErr.StatusCode == http.StatusNotFound {
@ -296,6 +313,22 @@ func (ra *RepositoryAPI) Delete() {
ra.CustomAbort(http.StatusInternalServerError, "") ra.CustomAbort(http.StatusInternalServerError, "")
} }
if !exist { if !exist {
repository, err := dao.GetRepositoryByName(repoName)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get repository %s: %v", repoName, err))
return
}
if repository == nil {
ra.HandleNotFound(fmt.Sprintf("repository %s not found", repoName))
return
}
if err = dao.DeleteLabelsOfResource(common.ResourceTypeRepository,
strconv.FormatInt(repository.RepositoryID, 10)); err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to delete labels of repository %s: %v",
repoName, err))
return
}
if err = dao.DeleteRepository(repoName); err != nil { if err = dao.DeleteRepository(repoName); err != nil {
log.Errorf("failed to delete repository %s: %v", repoName, err) log.Errorf("failed to delete repository %s: %v", repoName, err)
ra.CustomAbort(http.StatusInternalServerError, "") ra.CustomAbort(http.StatusInternalServerError, "")
@ -343,7 +376,7 @@ func (ra *RepositoryAPI) GetTag() {
return return
} }
result := assemble(client, repository, []string{tag}, result := assembleTags(client, repository, []string{tag},
ra.SecurityCtx.GetUsername()) ra.SecurityCtx.GetUsername())
ra.Data["json"] = result[0] ra.Data["json"] = result[0]
ra.ServeJSON() ra.ServeJSON()
@ -387,13 +420,13 @@ func (ra *RepositoryAPI) GetTags() {
return return
} }
ra.Data["json"] = assemble(client, repoName, tags, ra.SecurityCtx.GetUsername()) ra.Data["json"] = assembleTags(client, repoName, tags, ra.SecurityCtx.GetUsername())
ra.ServeJSON() ra.ServeJSON()
} }
// get config, signature and scan overview and assemble them into one // get config, signature and scan overview and assemble them into one
// struct for each tag in tags // struct for each tag in tags
func assemble(client *registry.Repository, repository string, func assembleTags(client *registry.Repository, repository string,
tags []string, username string) []*tagResp { tags []string, username string) []*tagResp {
var err error var err error
@ -435,6 +468,15 @@ func assemble(client *registry.Repository, repository string,
} }
} }
// labels
image := fmt.Sprintf("%s:%s", repository, t)
labels, err := dao.GetLabelsOfResource(common.ResourceTypeImage, image)
if err != nil {
log.Errorf("failed to get labels of image %s: %v", image, err)
} else {
item.Labels = labels
}
result = append(result, item) result = append(result, item)
} }
@ -633,7 +675,7 @@ func (ra *RepositoryAPI) GetTopRepos() {
ra.CustomAbort(http.StatusInternalServerError, "internal server error") ra.CustomAbort(http.StatusInternalServerError, "internal server error")
} }
result, err := populateTagsCount(repos) result, err := assembleRepos(repos)
if err != nil { if err != nil {
log.Errorf("failed to popultate tags count to repositories: %v", err) log.Errorf("failed to popultate tags count to repositories: %v", err)
ra.CustomAbort(http.StatusInternalServerError, "internal server error") ra.CustomAbort(http.StatusInternalServerError, "internal server error")

View File

@ -0,0 +1,248 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"strconv"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils"
uiutils "github.com/vmware/harbor/src/ui/utils"
)
// RepositoryLabelAPI handles requests for adding/removing label to/from repositories and images
type RepositoryLabelAPI struct {
BaseController
repository *models.RepoRecord
tag string
label *models.Label
}
// Prepare ...
func (r *RepositoryLabelAPI) Prepare() {
r.BaseController.Prepare()
if !r.SecurityCtx.IsAuthenticated() {
r.HandleUnauthorized()
return
}
repository := r.GetString(":splat")
project, _ := utils.ParseRepository(repository)
if !r.SecurityCtx.HasWritePerm(project) {
r.HandleForbidden(r.SecurityCtx.GetUsername())
return
}
repo, err := dao.GetRepositoryByName(repository)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get repository %s: %v",
repository, err))
return
}
if repo == nil {
r.HandleNotFound(fmt.Sprintf("repository %s not found", repository))
return
}
r.repository = repo
tag := r.GetString(":tag")
if len(tag) > 0 {
exist, err := imageExist(r.SecurityCtx.GetUsername(), repository, tag)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of image %s:%s: %v",
repository, tag, err))
return
}
if !exist {
r.HandleNotFound(fmt.Sprintf("image %s:%s not found", repository, tag))
return
}
r.tag = tag
}
if r.Ctx.Request.Method == http.MethodPost {
l := &models.Label{}
r.DecodeJSONReq(l)
label, err := dao.GetLabel(l.ID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", l.ID, err))
return
}
if label == nil {
r.HandleNotFound(fmt.Sprintf("label %d not found", l.ID))
return
}
if label.Level != common.LabelLevelUser {
r.HandleBadRequest("only user level labels can be used")
return
}
if label.Scope == common.LabelScopeProject {
p, err := r.ProjectMgr.Get(project)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get project %s: %v", project, err))
return
}
if p.ProjectID != label.ProjectID {
r.HandleBadRequest("can not add labels which don't belong to the project to the resources under the project")
return
}
}
r.label = label
return
}
if r.Ctx.Request.Method == http.MethodDelete {
labelID, err := r.GetInt64FromPath(":id")
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get ID parameter from path: %v", err))
return
}
label, err := dao.GetLabel(labelID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get label %d: %v", labelID, err))
return
}
if label == nil {
r.HandleNotFound(fmt.Sprintf("label %d not found", labelID))
return
}
r.label = label
}
}
// GetOfImage returns labels of an image
func (r *RepositoryLabelAPI) GetOfImage() {
r.getLabels(common.ResourceTypeImage, fmt.Sprintf("%s:%s", r.repository.Name, r.tag))
}
// AddToImage adds the label to an image
func (r *RepositoryLabelAPI) AddToImage() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeImage,
ResourceID: fmt.Sprintf("%s:%s", r.repository.Name, r.tag),
}
r.addLabel(rl)
}
// RemoveFromImage removes the label from an image
func (r *RepositoryLabelAPI) RemoveFromImage() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeImage,
ResourceID: fmt.Sprintf("%s:%s", r.repository.Name, r.tag),
}
r.removeLabel(rl)
}
// GetOfRepository returns labels of a repository
func (r *RepositoryLabelAPI) GetOfRepository() {
r.getLabels(common.ResourceTypeRepository, strconv.FormatInt(r.repository.RepositoryID, 10))
}
// AddToRepository adds the label to a repository
func (r *RepositoryLabelAPI) AddToRepository() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeRepository,
ResourceID: strconv.FormatInt(r.repository.RepositoryID, 10),
}
r.addLabel(rl)
}
// RemoveFromRepository removes the label from a repository
func (r *RepositoryLabelAPI) RemoveFromRepository() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeRepository,
ResourceID: strconv.FormatInt(r.repository.RepositoryID, 10),
}
r.removeLabel(rl)
}
func (r *RepositoryLabelAPI) getLabels(rType, rID string) {
labels, err := dao.GetLabelsOfResource(rType, rID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get labels of resource %s %s: %v",
rType, rID, err))
return
}
r.Data["json"] = labels
r.ServeJSON()
}
func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) {
rlabel, err := dao.GetResourceLabel(rl.ResourceType, rl.ResourceID, rl.LabelID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %s: %v",
rl.LabelID, rl.ResourceType, rl.ResourceID, err))
return
}
if rlabel != nil {
r.HandleConflict()
return
}
if _, err := dao.AddResourceLabel(rl); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to add label %d to resource %s %s: %v",
rl.LabelID, rl.ResourceType, rl.ResourceID, err))
return
}
// return the ID of label and return status code 200 rather than 201 as the label is not created
r.Redirect(http.StatusOK, strconv.FormatInt(rl.LabelID, 10))
}
func (r *RepositoryLabelAPI) removeLabel(rl *models.ResourceLabel) {
rlabel, err := dao.GetResourceLabel(rl.ResourceType, rl.ResourceID, rl.LabelID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %s: %v",
rl.LabelID, rl.ResourceType, rl.ResourceID, err))
return
}
if rlabel == nil {
r.HandleNotFound(fmt.Sprintf("label %d of resource %s %s not found",
rl.LabelID, rl.ResourceType, rl.ResourceID))
return
}
if err = dao.DeleteResourceLabel(rlabel.ID); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete resource label record %d: %v",
rlabel.ID, err))
return
}
}
func imageExist(username, repository, tag string) (bool, error) {
client, err := uiutils.NewRepositoryClientForUI(username, repository)
if err != nil {
return false, err
}
_, exist, err := client.ManifestExist(tag)
return exist, err
}

View File

@ -0,0 +1,253 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package api
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/dao"
"github.com/vmware/harbor/src/common/models"
)
var (
resourceLabelAPIBasePath = "/api/repositories"
repository = "library/hello-world"
tag = "latest"
proLibraryLabelID int64
)
func TestAddToImage(t *testing.T) {
sysLevelLabelID, err := dao.AddLabel(&models.Label{
Name: "sys_level_label",
Level: common.LabelLevelSystem,
})
require.Nil(t, err)
defer dao.DeleteLabel(sysLevelLabelID)
proTestLabelID, err := dao.AddLabel(&models.Label{
Name: "pro_test_label",
Level: common.LabelLevelUser,
Scope: common.LabelScopeProject,
ProjectID: 100,
})
require.Nil(t, err)
defer dao.DeleteLabel(proTestLabelID)
proLibraryLabelID, err = dao.AddLabel(&models.Label{
Name: "pro_library_label",
Level: common.LabelLevelUser,
Scope: common.LabelScopeProject,
ProjectID: 1,
})
require.Nil(t, err)
cases := []*codeCheckingCase{
// 401
&codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
method: http.MethodPost,
},
code: http.StatusUnauthorized,
},
// 403
&codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
method: http.MethodPost,
credential: projGuest,
},
code: http.StatusForbidden,
},
// 404 repository doesn't exist
&codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/library/non-exist-repo/tags/%s/labels", resourceLabelAPIBasePath, tag),
method: http.MethodPost,
credential: projDeveloper,
},
code: http.StatusNotFound,
},
// 404 image doesn't exist
&codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/non-exist-tag/labels", resourceLabelAPIBasePath, repository),
method: http.MethodPost,
credential: projDeveloper,
},
code: http.StatusNotFound,
},
// 400
&codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repository, tag),
method: http.MethodPost,
credential: projDeveloper,
},
code: http.StatusBadRequest,
},
// 404 label doesn't exist
&codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
ID int64
}{
ID: 1000,
},
},
code: http.StatusNotFound,
},
// 400 system level label
&codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
ID int64
}{
ID: sysLevelLabelID,
},
},
code: http.StatusBadRequest,
},
// 400 try to add the label of project1 to the image under project2
&codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
ID int64
}{
ID: proTestLabelID,
},
},
code: http.StatusBadRequest,
},
// 200
&codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
method: http.MethodPost,
credential: projDeveloper,
bodyJSON: struct {
ID int64
}{
ID: proLibraryLabelID,
},
},
code: http.StatusOK,
},
}
runCodeCheckingCases(t, cases...)
}
func TestGetOfImage(t *testing.T) {
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath, repository, tag),
method: http.MethodGet,
credential: projDeveloper,
}, &labels)
require.Nil(t, err)
require.Equal(t, 1, len(labels))
assert.Equal(t, proLibraryLabelID, labels[0].ID)
}
func TestRemoveFromImage(t *testing.T) {
runCodeCheckingCases(t, &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels/%d", resourceLabelAPIBasePath,
repository, tag, proLibraryLabelID),
method: http.MethodDelete,
credential: projDeveloper,
},
code: http.StatusOK,
})
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s/tags/%s/labels", resourceLabelAPIBasePath,
repository, tag),
method: http.MethodGet,
credential: projDeveloper,
}, &labels)
require.Nil(t, err)
require.Equal(t, 0, len(labels))
}
func TestAddToRepository(t *testing.T) {
runCodeCheckingCases(t, &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository),
method: http.MethodPost,
bodyJSON: struct {
ID int64
}{
ID: proLibraryLabelID,
},
credential: projDeveloper,
},
code: http.StatusOK,
})
}
func TestGetOfRepository(t *testing.T) {
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository),
method: http.MethodGet,
credential: projDeveloper,
}, &labels)
require.Nil(t, err)
require.Equal(t, 1, len(labels))
assert.Equal(t, proLibraryLabelID, labels[0].ID)
}
func TestRemoveFromRepository(t *testing.T) {
runCodeCheckingCases(t, &codeCheckingCase{
request: &testingRequest{
url: fmt.Sprintf("%s/%s/labels/%d", resourceLabelAPIBasePath,
repository, proLibraryLabelID),
method: http.MethodDelete,
credential: projDeveloper,
},
code: http.StatusOK,
})
labels := []*models.Label{}
err := handleAndParse(&testingRequest{
url: fmt.Sprintf("%s/%s/labels", resourceLabelAPIBasePath, repository),
method: http.MethodGet,
credential: projDeveloper,
}, &labels)
require.Nil(t, err)
require.Equal(t, 0, len(labels))
}

View File

@ -247,3 +247,9 @@ func getClairVulnStatus() *models.ClairVulnerabilityStatus {
res.Details = details res.Details = details
return res return res
} }
// Ping ping the harbor UI service.
func (sia *SystemInfoAPI) Ping() {
sia.Data["json"] = "Pong"
sia.ServeJSON()
}

View File

@ -91,3 +91,10 @@ func TestGetCert(t *testing.T) {
} }
CommonDelUser() CommonDelUser()
} }
func TestPing(t *testing.T) {
apiTest := newHarborAPI()
code, _, err := apiTest.Ping()
assert := assert.New(t)
assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err))
assert.Equal(200, code, fmt.Sprintf("Unexpected status code: %d", code))
}

View File

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"github.com/vmware/harbor/src/adminserver/client" "github.com/vmware/harbor/src/adminserver/client"
@ -205,6 +206,35 @@ func LDAPConf() (*models.LdapConf, error) {
return ldapConf, nil return ldapConf, nil
} }
// LDAPGroupConf returns the setting of ldap group search
func LDAPGroupConf() (*models.LdapGroupConf, error) {
cfg, err := mg.Get()
if err != nil {
return nil, err
}
ldapGroupConf := &models.LdapGroupConf{LdapGroupSearchScope: 2}
if _, ok := cfg[common.LDAPGroupBaseDN]; ok {
ldapGroupConf.LdapGroupBaseDN = cfg[common.LDAPGroupBaseDN].(string)
}
if _, ok := cfg[common.LDAPGroupSearchFilter]; ok {
ldapGroupConf.LdapGroupFilter = cfg[common.LDAPGroupSearchFilter].(string)
}
if _, ok := cfg[common.LDAPGroupAttributeName]; ok {
ldapGroupConf.LdapGroupNameAttribute = cfg[common.LDAPGroupAttributeName].(string)
}
if _, ok := cfg[common.LDAPGroupSearchScope]; ok {
if scopeStr, ok := cfg[common.LDAPGroupSearchScope].(string); ok {
ldapGroupConf.LdapGroupSearchScope, err = strconv.Atoi(scopeStr)
}
if scopeFloat, ok := cfg[common.LDAPGroupSearchScope].(float64); ok {
ldapGroupConf.LdapGroupSearchScope = int(scopeFloat)
}
}
return ldapGroupConf, nil
}
// TokenExpiration returns the token expiration time (in minute) // TokenExpiration returns the token expiration time (in minute)
func TokenExpiration() (int, error) { func TokenExpiration() (int, error) {
cfg, err := mg.Get() cfg, err := mg.Get()

View File

@ -75,6 +75,10 @@ func TestConfig(t *testing.T) {
t.Fatalf("failed to get ldap settings: %v", err) t.Fatalf("failed to get ldap settings: %v", err)
} }
if _, err := LDAPGroupConf(); err != nil {
t.Fatalf("failed to get ldap group settings: %v", err)
}
if _, err := TokenExpiration(); err != nil { if _, err := TokenExpiration(); err != nil {
t.Fatalf("failed to get token expiration: %v", err) t.Fatalf("failed to get token expiration: %v", err)
} }

View File

@ -60,6 +60,7 @@ func initRouters() {
} }
// API // API
beego.Router("/api/ping", &api.SystemInfoAPI{}, "get:Ping")
beego.Router("/api/search", &api.SearchAPI{}) beego.Router("/api/search", &api.SearchAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post") beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post")
beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs") beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs")
@ -70,7 +71,11 @@ func initRouters() {
beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get") beego.Router("/api/repositories", &api.RepositoryAPI{}, "get:Get")
beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll") beego.Router("/api/repositories/scanAll", &api.RepositoryAPI{}, "post:ScanAll")
beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put") beego.Router("/api/repositories/*", &api.RepositoryAPI{}, "delete:Delete;put:Put")
beego.Router("/api/repositories/*/labels", &api.RepositoryLabelAPI{}, "get:GetOfRepository;post:AddToRepository")
beego.Router("/api/repositories/*/labels/:id([0-9]+)", &api.RepositoryLabelAPI{}, "delete:RemoveFromRepository")
beego.Router("/api/repositories/*/tags/:tag", &api.RepositoryAPI{}, "delete:Delete;get:GetTag") beego.Router("/api/repositories/*/tags/:tag", &api.RepositoryAPI{}, "delete:Delete;get:GetTag")
beego.Router("/api/repositories/*/tags/:tag/labels", &api.RepositoryLabelAPI{}, "get:GetOfImage;post:AddToImage")
beego.Router("/api/repositories/*/tags/:tag/labels/:id([0-9]+)", &api.RepositoryLabelAPI{}, "delete:RemoveFromImage")
beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags") beego.Router("/api/repositories/*/tags", &api.RepositoryAPI{}, "get:GetTags")
beego.Router("/api/repositories/*/tags/:tag/scan", &api.RepositoryAPI{}, "post:ScanImage") beego.Router("/api/repositories/*/tags/:tag/scan", &api.RepositoryAPI{}, "post:ScanImage")
beego.Router("/api/repositories/*/tags/:tag/vulnerability/details", &api.RepositoryAPI{}, "Get:VulnerabilityDetails") beego.Router("/api/repositories/*/tags/:tag/vulnerability/details", &api.RepositoryAPI{}, "Get:VulnerabilityDetails")
@ -94,6 +99,8 @@ func initRouters() {
beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset") beego.Router("/api/configurations/reset", &api.ConfigAPI{}, "post:Reset")
beego.Router("/api/statistics", &api.StatisticAPI{}) beego.Router("/api/statistics", &api.StatisticAPI{})
beego.Router("/api/replications", &api.ReplicationAPI{}) beego.Router("/api/replications", &api.ReplicationAPI{})
beego.Router("/api/labels", &api.LabelAPI{}, "post:Post;get:List")
beego.Router("/api/labels/:id([0-9]+)", &api.LabelAPI{}, "get:Get;put:Put;delete:Delete")
beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo") beego.Router("/api/systeminfo", &api.SystemInfoAPI{}, "get:GetGeneralInfo")
beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo") beego.Router("/api/systeminfo/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
@ -108,6 +115,7 @@ func initRouters() {
beego.Router("/service/token", &token.Handler{}) beego.Router("/service/token", &token.Handler{})
beego.Router("/registryproxy/*", &controllers.RegistryProxy{}, "*:Handle") beego.Router("/registryproxy/*", &controllers.RegistryProxy{}, "*:Handle")
//Error pages //Error pages
beego.ErrorController(&controllers.ErrorController{}) beego.ErrorController(&controllers.ErrorController{})

View File

@ -74,13 +74,14 @@ Use **withTitle** to set whether self-contained a header with title or not. Defa
Support two different display scope mode: under specific project or whole system. Support two different display scope mode: under specific project or whole system.
If **projectId** is set to the id of specified project, then only show the replication rules bound with the project. Otherwise, show all the rules of the whole system. If **projectId** is set to the id of specified project, then only show the replication rules bound with the project. Otherwise, show all the rules of the whole system.
On specific project mode, without need projectId, but also need to provide projectName for display.
**withReplicationJob** is used to determine whether or not show the replication jobs which are relevant with the selected replication rule. **withReplicationJob** is used to determine whether or not show the replication jobs which are relevant with the selected replication rule.
**readonly** is to disable all the create/edit/delete actions. **isSystemAdmin** is for judgment if user has administrator privilege, if true, user can do the create/edit/delete/replicate actions.
``` ```
<hbr-replication [projectId]="..." [withReplicationJob]='...' [readonly]="..."></hbr-replication> <hbr-replication [projectId]="..." [projectName]="..." [withReplicationJob]='...' [isSystemAdmin]="..."></hbr-replication>
``` ```
* **Endpoint Management View** * **Endpoint Management View**

View File

@ -1,5 +1,70 @@
export const CREATE_EDIT_RULE_STYLE: string = ` export const CREATE_EDIT_RULE_STYLE: string = `
.form-group-label-override { /**
font-size: 14px; * Created by pengf on 9/28/2017.
font-weight: 400; */
}`;
.select{
width: 186px;
}
.select .optionMore{
background-color: #bfbaba;
height: 1.6em;
font-size: 1.2em;
cursor: pointer;
text-align: center;
}
.hideFilter{ display: none;}
h4{
color: #666;
}
.colorRed{color: red;}
.colorRed a{text-decoration: underline;color: #007CBB;}
.alertLabel{display:block; margin-top:0; line-height:1em; font-size:12px;}
.inputWidth{width: 270px;}
.endpointSelect{ width: 270px; margin-right: 20px;}
.filterSelect{width: 315px;}
.filterSelect clr-icon{margin-left: 15px;}
.filterSelect label{width: 136px;}
.filterSelect label input{width: 100%;}
.pull-left{float: left;}
.padLeft0{padding-left: 0;}
.floatSetPar{display: inline-block; width: 120px;margin-right: 10px;}
.floatSet {display: inline-block; width: 82px;margin-right: 4px;}
.form-group{ min-height: 36px;}
.projectInput{float: left;position: relative;}
.switchIcon{width:20px;height:20px; margin-top: 10px;margin-left: 10px; cursor: pointer;}
.addEndpoint{ margin-top: .25em !important;padding-left:2px;padding-right:2px;min-width:58px;margin-right:0}
.shadow{position: absolute;top: 8px;}
.is-solid{cursor: pointer;}
.selectBox{
position: absolute;
width: 100%;
height: auto;
margin-top:-0.25rem;
border: 1px solid #ccc;
background-color: white;
border: 1px solid rgba(0,0,0,.15);
border-right-width: 2px;
border-bottom-width: 2px;
border-radius: 6px;
box-shadow: 0 5px 10px rgba(0,0,0,.2);
z-index: 100;
}
.selectBox ul li{
list-style: none;
padding: 3px 20px
cursor: pointer;
}
.selectBox ul li:hover{
color: #262626;
background-image: linear-gradient(180deg,#f5f5f5 0,#e8e8e8);
background-repeat: repeat-x;
}
.form-group-override{
padding-left: 170px !important;
}
.form-group>label:first-child{font-size:14px; width:6.5rem;}
.goLink{color:blue; border-bottom:1px solid blue; line-height:14px; cursor:pointer;}
`;

View File

@ -1,100 +1,134 @@
export const CREATE_EDIT_RULE_TEMPLATE: string = ` export const CREATE_EDIT_RULE_TEMPLATE: string = `
<clr-modal [(clrModalOpen)]="createEditRuleOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable"> <clr-modal [(clrModalOpen)]="createEditRuleOpened" [clrModalStaticBackdrop]="true" [clrModalClosable]="false">
<h3 class="modal-title">{{modalTitle}}</h3> <h3 class="modal-title">{{headerTitle | translate}}</h3>
<hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert> <hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
<div class="modal-body" style="max-height: 85vh;"> <div class="modal-body" style="max-height: 85vh;">
<form #ruleForm="ngForm"> <form [formGroup]="ruleForm" novalidate>
<section class="form-block"> <section class="form-block">
<div class="alert alert-warning" *ngIf="!editable"> <div class="form-group form-group-override">
<div class="alert-item static"> <label class="form-group-label-override required">{{'REPLICATION.NAME' | translate}}</label>
<div class="alert-icon-wrapper"> <label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
<clr-icon class="alert-icon" shape="exclamation-circle"></clr-icon> [class.invalid]='(ruleForm.controls.name.touched && ruleForm.controls.name.invalid) || isRuleNameExist'>
<input type="text" id="ruleName" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" class="inputWidth" required maxlength="255" formControlName="name" #ruleName (keyup)='checkRuleName()' autocomplete="off">
<span class="tooltip-content">{{ruleNameTooltip | translate}}</span>
</label><span class="spinner spinner-inline spinner-pos" [hidden]="!inNameChecking"></span>
</div> </div>
<span class="alert-text"> <!--Description-->
{{'REPLICATION.CANNOT_EDIT' | translate}} <div class="form-group form-group-override">
</span> <label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<textarea type="text" id="ruleDescription" class="inputWidth" row= 3; formControlName="description"></textarea>
</div>
<!--Projects-->
<div class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.SOURCE' | translate}}&nbsp;{{'PROJECT.PROJECTS' | translate | lowercase}}</label>
<div formArrayName="projects">
<div class="projectInput inputWidth" *ngFor="let project of projects.controls; let i= index" [formGroupName]="i" (mouseleave)="leaveInput()">
<input *ngIf="!projectId" formControlName="name" type="text" class="inputWidth" value="name" required
pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" (keyup)='handleValidation()' (focus)="focusClear($event)" autocomplete="off">
<input *ngIf="projectId" formControlName="name" type="text" class="inputWidth" value="name" readonly>
<div class="selectBox inputWidth" [style.display]="selectedProjectList.length ? 'block' : 'none'" >
<ul>
<li *ngFor="let project of selectedProjectList" (click)="selectedProjectName(project?.name)">{{project?.name}}</li>
</ul>
</div> </div>
</div> </div>
<div class="form-group">
<label for="policy_name" class="col-md-4 form-group-label-override">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label>
<label for="policy_name" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="name.errors && (name.dirty || name.touched)" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="policy_name" [(ngModel)]="createEditRule.name" name="name" size="20" #name="ngModel" required [readonly]="readonly">
<span class="tooltip-content" *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)">
{{'REPLICATION.NAME_IS_REQUIRED' | translate}}
</span>
</label>
</div> </div>
<div class="form-group"> <label *ngIf="noProjectInfo.length != 0" class="colorRed alertLabel">{{noProjectInfo | translate}}</label>
<label for="policy_description" class="col-md-4 form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<textarea class="col-md-8" id="policy_description" row="3" [(ngModel)]="createEditRule.description" name="description" size="20" #description="ngModel" [readonly]="readonly"></textarea>
</div> </div>
<div class="form-group">
<label class="col-md-4 form-group-label-override">{{'REPLICATION.ENABLE' | translate}}</label> <!--images/Filter-->
<div class="checkbox-inline"> <div class="form-group form-group-override">
<input type="checkbox" id="policy_enable" [(ngModel)]="createEditRule.enable" name="enable" #enable="ngModel" [disabled]="untoggleable"> <label class="form-group-label-override">{{'REPLICATION.SOURCE_IMAGES_FILTER' | translate}}</label>
<label for="policy_enable"></label> <div formArrayName="filters">
</div> <div class="filterSelect" *ngFor="let filter of filters.controls; let i=index" [formGroupName]="i">
</div> <div>
<div class="form-group"> <div class="select floatSetPar">
<label for="destination_name" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_NAME' | translate}}<span style="color: red">*</span></label> <select formControlName="kind" (change)="filterChange($event)" id="{{i}}" name="{{filterListData[i]?.name}}">
<div class="select" *ngIf="!isCreateEndpoint"> <option *ngFor="let filter of filterListData[i]?.options;" value="{{filter}}">{{filter}}</option>
<select id="destination_name" [(ngModel)]="createEditRule.endpointId" name="endpointId" (change)="selectEndpoint()" [disabled]="testOngoing || readonly">
<option *ngFor="let t of endpoints" [value]="t.id" [selected]="t.id == createEditRule.endpointId">{{t.name}}</option>
</select> </select>
</div> </div>
<label class="col-md-8" *ngIf="isCreateEndpoint" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="endpointName.errors && (endpointName.dirty || endpointName.touched)" <label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left"> [class.invalid]='ruleForm.controls.filters.controls[i].controls.pattern.touched && ruleForm.controls.filters.controls[i].controls.pattern.invalid'>
<input type="text" id="destination_name" [(ngModel)]="createEditRule.endpointName" name="endpointName" size="8" #endpointName="ngModel" value="" required> <input type="text" #filterValue required size="14" formControlName="pattern">
<span class="tooltip-content" *ngIf="endpointName.errors && endpointName.errors.required && (endpointName.dirty || endpointName.touched)"> <span class="tooltip-content">{{'TOOLTIP.EMPTY' | translate}}</span>
{{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}}
</span>
</label> </label>
<div class="checkbox-inline" *ngIf="showNewDestination"> <clr-icon shape="times-circle" class="is-solid" (click)="deleteFilter(i)"></clr-icon>
<input type="checkbox" id="check_new" (click)="newEndpoint(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateEndpoint" [disabled]="testOngoing || readonly">
<label for="check_new">{{'REPLICATION.NEW_DESTINATION' | translate}}</label>
</div> </div>
</div> </div>
<div class="form-group">
<label for="destination_url" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_URL' | translate}}<span style="color: red">*</span></label>
<label for="destination_url" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="endpointUrl.errors && (endpointUrl.dirty || endpointUrl.touched)"
class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="destination_url" [disabled]="testOngoing" [readonly]="readonly || !isCreateEndpoint"
[(ngModel)]="createEditRule.endpointUrl" size="20" name="endpointUrl" required #endpointUrl="ngModel">
<span class="tooltip-content" *ngIf="endpointUrl.errors && endpointUrl.errors.required && (endpointUrl.dirty || endpointUrl.touched)">
{{'REPLICATION.DESTINATION_URL_IS_REQUIRED' | translate}}
</span>
</label>
</div> </div>
<div class="form-group"> <clr-icon shape="plus-circle" class="is-solid" [hidden]="isFilterHide" (click)="addNewFilter()" style="margin-top: 11px;"></clr-icon>
<label for="destination_username" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_USERNAME' | translate}}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [readonly]="readonly || !isCreateEndpoint"
[(ngModel)]="createEditRule.username" size="20" name="username" #username="ngModel">
</div> </div>
<div class="form-group"> <!--Targets-->
<label for="destination_password" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label> <div class="form-group form-group-override">
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="readonly || !isCreateEndpoint" <label class="form-group-label-override required">{{'DESTINATION.ENDPOINT' | translate}}</label>
[(ngModel)]="createEditRule.password" size="20" name="password" #password="ngModel"> <div formArrayName="targets">
<div class="select endpointSelect pull-left" *ngFor="let target of targets.controls; let i= index" [formGroupName]="i">
<select id="ruleTarget" (change)="targetChange($event)" formControlName="id">
<option *ngFor="let target of targetList" value="{{target.id}}">{{target.name}}-{{target.endpoint}}</option>
</select>
</div> </div>
<div class="form-group"> </div>
<label for="destination_insecure" class="col-md-4 form-group-label-override">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label> <label *ngIf="noEndpointInfo.length != 0" class="colorRed alertLabel">{{noEndpointInfo | translate}}</label>
<clr-checkbox #insecure class="col-md-8" name="insecure" id="destination_insecure" [clrChecked]="!createEditRule.insecure" [clrDisabled]="readonly || !isCreateEndpoint || testOngoing" (clrCheckedChange)="setInsecureValue($event)"> <span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}}</span>
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-7px;"> </div>
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate}}</span> <!--Trigger-->
</a> <div class="form-group form-group-override">
<label class="form-group-label-override">{{'REPLICATION.TRIGGER_MODE' | translate}}</label>
<div formGroupName="trigger">
<!--on trigger-->
<div class="select floatSetPar">
<select id="ruleTrigger" formControlName="kind" (change)="selectTrigger($event)">
<option value="Manual">{{'REPLICATION.MANUAL' | translate}}</option>
<option value="Immediate">{{'REPLICATION.IMMEDIATE' | translate}}</option>
<option value="Scheduled">{{'REPLICATION.SCHEDULE' | translate}}</option>
</select>
</div>
<!--on push-->
<div formGroupName="schedule_param">
<div class="select floatSet" [hidden]="!isScheduleOpt">
<select name="scheduleType" formControlName="type" (change)="selectSchedule($event)">
<option value="Daily">{{'REPLICATION.DAILY' | translate}}</option>
<option value="Weekly">{{'REPLICATION.WEEKLY' | translate}}</option>
</select>
</div>
<!--weekly-->
<span [hidden]="!weeklySchedule || !isScheduleOpt">on &nbsp;</span>
<div [hidden]="!weeklySchedule || !isScheduleOpt" class="select floatSet" style="width:104px">
<select name="scheduleDay" formControlName="weekday">
<option value="1">{{'WEEKLY.MONDAY' | translate}}</option>
<option value="2">{{'WEEKLY.TUESDAY' | translate}}</option>
<option value="3">{{'WEEKLY.WEDNESDAY' | translate}}</option>
<option value="4">{{'WEEKLY.THURSDAY' | translate}}</option>
<option value="5">{{'WEEKLY.FRIDAY' | translate}}</option>
<option value="6">{{'WEEKLY.SATURDAY' | translate}}</option>
<option value="7">{{'WEEKLY.SUNDAY' | translate}}</option>
</select>
</div>
<!--daily/time-->
<span [hidden]="!isScheduleOpt">at &nbsp;</span>
<input [hidden]="!isScheduleOpt" type="time" formControlName="offtime" required value="08:00" />
</div>
</div>
<div style="width: 100%;" [hidden]="!isImmediate">
<clr-checkbox [clrChecked]="false" id="ruleDeletion" formControlName="replicate_deletion">
{{'REPLICATION.DELETE_REMOTE_IMAGES' | translate}}
</clr-checkbox> </clr-checkbox>
</div> </div>
<div class="form-group"> <div style="width: 100%;" >
<label for="spin" class="col-md-4"></label> <clr-checkbox [clrChecked]="true" id="ruleExit" formControlName="replicate_existing_image_now">
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span> {{'REPLICATION.REPLICATE_IMMEDIATE' | translate}}
<span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span> </clr-checkbox>
</div>
</div>
<div style="display:block;text-align:center">
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
</div> </div>
</section> </section>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing || endpointUrl.errors || connectAbled">{{'REPLICATION.TEST_CONNECTION' | translate}}</button> <button type="button" id="ruleBtnCancel" class="btn btn-outline" [disabled]="this.inProgress" (click)="onCancel()">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="button" class="btn btn-outline" [disabled]="btnAbled" (click)="onCancel()">{{'BUTTON.CANCEL' | translate }}</button> <button type="submit" id="ruleBtnOk" class="btn btn-primary" (click)="onSubmit()" [disabled]="!ruleForm.valid || !isValid || !hasFormChange()">{{ 'BUTTON.SAVE' | translate }}</button>
<button type="submit" class="btn btn-primary" [disabled]="!ruleForm.form.valid || testOngoing || !editable" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
</div> </div>
</clr-modal>`; </clr-modal>`;

View File

@ -25,52 +25,55 @@ import {
JobLogDefaultService JobLogDefaultService
} from '../service/index'; } from '../service/index';
import { EndpointService, EndpointDefaultService } from '../service/endpoint.service'; import { EndpointService, EndpointDefaultService } from '../service/endpoint.service';
import {ProjectDefaultService, ProjectService} from "../service/project.service";
import { JobLogViewerComponent } from '../job-log-viewer/job-log-viewer.component'; import { JobLogViewerComponent } from '../job-log-viewer/job-log-viewer.component';
import {Project} from "../project-policy-config/project";
describe('CreateEditRuleComponent (inline template)', ()=>{ describe('CreateEditRuleComponent (inline template)', ()=>{
let mockRules: ReplicationRule[] = [ let mockRules: ReplicationRule[] = [
{ {
"id": 1, "id": 1,
"project_id": 1,
"project_name": "library",
"target_id": 1,
"target_name": "target_01",
"name": "sync_01", "name": "sync_01",
"enabled": 0,
"description": "", "description": "",
"cron_str": "", "projects": [{ "project_id": 1,
"error_job_count": 2, "owner_id": 0,
"deleted": 0 "name": 'project_01',
}, "creation_time": '',
{ "deleted": 0,
"id": 2, "owner_name": '',
"project_id": 1, "togglable": false,
"project_name": "library", "update_time": '',
"target_id": 3, "current_user_role_id": 0,
"target_name": "target_02", "repo_count": 0,
"name": "sync_02", "has_project_admin_role": false,
"enabled": 1, "is_member": false,
"description": "", "role_name": '',
"cron_str": "", "metadata": {
"error_job_count": 1, "public": '',
"deleted": 0 "enable_content_trust": '',
}, "prevent_vul": '',
{ "severity": '',
"id": 3, "auto_scan": '',
"project_id": 1,
"project_name": "library",
"target_id": 2,
"target_name": "target_03",
"name": "sync_03",
"enabled": 0,
"description": "",
"cron_str": "",
"error_job_count": 0,
"deleted": 0
} }
]; }],
"targets": [{
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"insecure": false,
"type": 0
}],
"trigger": {
"kind": "Manual",
"schedule_param": null
},
"filters": [],
"replicate_existing_image_now": false,
"replicate_deletion": false,
}]
let mockJobs: ReplicationJobItem[] = [ let mockJobs: ReplicationJobItem[] = [
{ {
"id": 1, "id": 1,
@ -144,17 +147,68 @@ describe('CreateEditRuleComponent (inline template)', ()=>{
let mockRule: ReplicationRule = { let mockRule: ReplicationRule = {
"id": 1, "id": 1,
"project_id": 1,
"project_name": "library",
"target_id": 1,
"target_name": "target_01",
"name": "sync_01", "name": "sync_01",
"enabled": 0,
"description": "", "description": "",
"cron_str": "", "projects": [{ "project_id": 1,
"error_job_count": 2, "owner_id": 0,
"deleted": 0 "name": 'project_01',
}; "creation_time": '',
"deleted": 0,
"owner_name": '',
"togglable": false,
"update_time": '',
"current_user_role_id": 0,
"repo_count": 0,
"has_project_admin_role": false,
"is_member": false,
"role_name": '',
"metadata": {
"public": '',
"enable_content_trust": '',
"prevent_vul": '',
"severity": '',
"auto_scan": '',
}
}],
"targets": [{
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"insecure": false,
"type": 0
}],
"trigger": {
"kind": "Manual",
"schedule_param": null
},
"filters": [],
"replicate_existing_image_now": false,
"replicate_deletion": false,
}
let mockProjects: Project[] = [
{ "project_id": 1,
"owner_id": 0,
"name": 'project_01',
"creation_time": '',
"deleted": 0,
"owner_name": '',
"togglable": false,
"update_time": '',
"current_user_role_id": 0,
"repo_count": 0,
"has_project_admin_role": false,
"is_member": false,
"role_name": '',
"metadata": {
"public": '',
"enable_content_trust": '',
"prevent_vul": '',
"severity": '',
"auto_scan": '',
}
}];
let fixture: ComponentFixture<ReplicationComponent>; let fixture: ComponentFixture<ReplicationComponent>;
let fixtureCreate: ComponentFixture<CreateEditRuleComponent>; let fixtureCreate: ComponentFixture<CreateEditRuleComponent>;
@ -172,12 +226,11 @@ describe('CreateEditRuleComponent (inline template)', ()=>{
let spyEndpoint: jasmine.Spy; let spyEndpoint: jasmine.Spy;
let config: IServiceConfig = { let config: IServiceConfig = {
replicationRuleEndpoint: '/api/policies/replication/testing',
replicationJobEndpoint: '/api/jobs/replication/testing', replicationJobEndpoint: '/api/jobs/replication/testing',
targetBaseEndpoint: '/api/targets/testing' targetBaseEndpoint: '/api/targets/testing'
}; };
beforeEach(async(()=>{ beforeEach(async(() =>{
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
SharedModule, SharedModule,
@ -198,6 +251,7 @@ describe('CreateEditRuleComponent (inline template)', ()=>{
{ provide: SERVICE_CONFIG, useValue: config }, { provide: SERVICE_CONFIG, useValue: config },
{ provide: ReplicationService, useClass: ReplicationDefaultService }, { provide: ReplicationService, useClass: ReplicationDefaultService },
{ provide: EndpointService, useClass: EndpointDefaultService }, { provide: EndpointService, useClass: EndpointDefaultService },
{ provide: ProjectService, useClass: ProjectDefaultService },
{ provide: JobLogService, useClass: JobLogDefaultService } { provide: JobLogService, useClass: JobLogDefaultService }
] ]
}); });
@ -205,28 +259,27 @@ describe('CreateEditRuleComponent (inline template)', ()=>{
beforeEach(()=>{ beforeEach(()=>{
fixture = TestBed.createComponent(ReplicationComponent); fixture = TestBed.createComponent(ReplicationComponent);
fixtureCreate = TestBed.createComponent(CreateEditRuleComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compCreate = fixtureCreate.componentInstance;
comp.projectId = 1; comp.projectId = 1;
comp.search.ruleId = 1; comp.search.ruleId = 1;
replicationService = fixture.debugElement.injector.get(ReplicationService); replicationService = fixture.debugElement.injector.get(ReplicationService);
endpointService = fixtureCreate.debugElement.injector.get(EndpointService) ;
spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(Promise.resolve(mockRules)); spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(Promise.resolve(mockRules));
spyOneRule = spyOn(replicationService, 'getReplicationRule').and.returnValue(Promise.resolve(mockRule)); spyOneRule = spyOn(replicationService, 'getReplicationRule').and.returnValue(Promise.resolve(mockRule));
spyJobs = spyOn(replicationService, 'getJobs').and.returnValues(Promise.resolve(mockJob)); spyJobs = spyOn(replicationService, 'getJobs').and.returnValues(Promise.resolve(mockJob));
fixture.detectChanges();
});
beforeEach(()=>{
fixtureCreate = TestBed.createComponent(CreateEditRuleComponent);
compCreate = fixtureCreate.componentInstance;
compCreate.projectId = 1;
endpointService = fixtureCreate.debugElement.injector.get(EndpointService);
spyEndpoint = spyOn(endpointService, 'getEndpoints').and.returnValues(Promise.resolve(mockEndpoints)); spyEndpoint = spyOn(endpointService, 'getEndpoints').and.returnValues(Promise.resolve(mockEndpoints));
fixture.detectChanges(); fixture.detectChanges();
}); });
it('Should open creation modal and load endpoints', async(()=>{ it('Should open creation modal and load endpoints', async(()=>{

View File

@ -1,4 +1,4 @@
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core'; import {Component, Input, Output, EventEmitter, ViewChild, OnChanges} from '@angular/core';
import { NgModel } from '@angular/forms'; import { NgModel } from '@angular/forms';
import { DATETIME_PICKER_TEMPLATE } from './datetime-picker.component.html'; import { DATETIME_PICKER_TEMPLATE } from './datetime-picker.component.html';
@ -7,7 +7,7 @@ import { DATETIME_PICKER_TEMPLATE } from './datetime-picker.component.html';
selector: 'hbr-datetime', selector: 'hbr-datetime',
template: DATETIME_PICKER_TEMPLATE template: DATETIME_PICKER_TEMPLATE
}) })
export class DatePickerComponent { export class DatePickerComponent implements OnChanges{
@Input() dateInput: string; @Input() dateInput: string;
@Input() oneDayOffset: boolean; @Input() oneDayOffset: boolean;
@ -17,6 +17,10 @@ export class DatePickerComponent {
@Output() search = new EventEmitter<string>(); @Output() search = new EventEmitter<string>();
ngOnChanges(): void {
this.dateInput = this.dateInput.trim();
}
get dateInvalid(): boolean { get dateInvalid(): boolean {
return (this.searchTime.errors && this.searchTime.errors.dateValidator && (this.searchTime.dirty || this.searchTime.touched)) || false; return (this.searchTime.errors && this.searchTime.errors.dateValidator && (this.searchTime.dirty || this.searchTime.touched)) || false;
} }

View File

@ -137,23 +137,13 @@ export class EndpointComponent implements OnInit, OnDestroy {
editTargets(targets: Endpoint[]) { editTargets(targets: Endpoint[]) {
if (targets && targets.length === 1) { if (targets && targets.length === 1) {
let target= targets[0]; let target = targets[0];
let editable = true; let editable = true;
if (!target.id) { if (!target.id) {
return; return;
} }
let id: number | string = target.id; let id: number | string = target.id;
toPromise<ReplicationRule[]>(this.endpointService
.getEndpointWithReplicationRules(id))
.then(
rules => {
if (rules && rules.length > 0) {
rules.forEach((rule) => editable = (rule && rule.enabled !== 1));
}
this.createEditEndpointComponent.openCreateEditTarget(editable, id); this.createEditEndpointComponent.openCreateEditTarget(editable, id);
this.forceRefreshView(1000);
})
.catch(error => this.errorHandler.error(error));
} }
} }

View File

@ -8,6 +8,7 @@ export * from './filter/index';
export * from './endpoint/index'; export * from './endpoint/index';
export * from './repository/index'; export * from './repository/index';
export * from './create-edit-endpoint/index'; export * from './create-edit-endpoint/index';
export * from './create-edit-rule/index';
export * from './repository-stackview/index'; export * from './repository-stackview/index';
export * from './tag/index'; export * from './tag/index';
export * from './list-replication-rule/index'; export * from './list-replication-rule/index';

View File

@ -1,11 +1,11 @@
export const LIST_REPLICATION_RULE_TEMPLATE: string = ` export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<div style="padding-bottom: 15px;"> <div style="padding-bottom: 15px;">
<clr-datagrid [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedRow" [clDgRowSelection]="true"> <clr-datagrid [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedRow" [clDgRowSelection]="true">
<clr-dg-action-bar style="height:24px;" *ngIf="opereateAvailable || isSystemAdmin"> <clr-dg-action-bar style="height:24px;">
<button type="button" class="btn btn-sm btn-secondary" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="editRule(selectedRow)"><clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'REPLICATION.EDIT_POLICY' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="editRule(selectedRow)"><clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPLICATION.DELETE_POLICY' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPLICATION.DELETE_POLICY' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow" (click)="replicateRule(selectedRow)"><clr-icon shape="export" size="16"></clr-icon>&nbsp;{{'REPLICATION.REPLICATE' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="replicateRule(selectedRow)"><clr-icon shape="export" size="16"></clr-icon>&nbsp;{{'REPLICATION.REPLICATE' | translate}}</button>
</clr-dg-action-bar> </clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'projects'" *ngIf="!projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'projects'" *ngIf="!projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
@ -35,7 +35,6 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<clr-dg-pagination #pagination [clrDgPageSize]="5"></clr-dg-pagination> <clr-dg-pagination #pagination [clrDgPageSize]="5"></clr-dg-pagination>
</clr-dg-footer> </clr-dg-footer>
</clr-datagrid> </clr-datagrid>
<confirmation-dialog #toggleConfirmDialog [batchInfors]="batchDelectionInfos" (confirmAction)="toggleConfirm($event)"></confirmation-dialog>
<confirmation-dialog #deletionConfirmDialog [batchInfors]="batchDelectionInfos" (confirmAction)="deletionConfirm($event)"></confirmation-dialog> <confirmation-dialog #deletionConfirmDialog [batchInfors]="batchDelectionInfos" (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
</div> </div>
`; `;

View File

@ -14,65 +14,90 @@ import { ErrorHandler } from '../error-handler/error-handler';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { ReplicationService, ReplicationDefaultService } from '../service/replication.service'; import { ReplicationService, ReplicationDefaultService } from '../service/replication.service';
describe('ListReplicationRuleComponent (inline template)', ()=>{ describe('ListReplicationRuleComponent (inline template)', ()=>{
let mockRules: ReplicationRule[] = [ let mockRules: ReplicationRule[] = [
{ {
"id": 1, "id": 1,
"project_id": 1, "projects": [{
"project_name": "library", "project_id": 33,
"target_id": 1, "owner_id": 1,
"target_name": "target_01", "name": "aeas",
"deleted": 0,
"togglable": false,
"current_user_role_id": 0,
"repo_count": 0,
"metadata": {
"public": false,
"enable_content_trust": "",
"prevent_vul": "",
"severity": "",
"auto_scan": ""},
"owner_name": "",
"creation_time": null,
"update_time": null,
"has_project_admin_role": true,
"is_member": true,
"role_name": ""
}],
"targets": [{
"endpoint": "",
"id": 0,
"insecure": false,
"name": "khans3",
"username": "",
"password": "",
"type": 0,
}],
"name": "sync_01", "name": "sync_01",
"enabled": 0,
"description": "", "description": "",
"cron_str": "", "filters": null,
"trigger": {"kind": "Manual", "schedule_param": null},
"error_job_count": 2, "error_job_count": 2,
"deleted": 0 "replicate_deletion": false,
"replicate_existing_image_now": false,
}, },
{ {
"id": 2, "id": 2,
"project_id": 1, "projects": [{
"project_name": "library", "project_id": 33,
"target_id": 3, "owner_id": 1,
"target_name": "target_02", "name": "aeas",
"deleted": 0,
"togglable": false,
"current_user_role_id": 0,
"repo_count": 0,
"metadata": {
"public": false,
"enable_content_trust": "",
"prevent_vul": "",
"severity": "",
"auto_scan": ""},
"owner_name": "",
"creation_time": null,
"update_time": null,
"has_project_admin_role": true,
"is_member": true,
"role_name": ""
}],
"targets": [{
"endpoint": "",
"id": 0,
"insecure": false,
"name": "khans3",
"username": "",
"password": "",
"type": 0,
}],
"name": "sync_02", "name": "sync_02",
"enabled": 1,
"description": "", "description": "",
"cron_str": "", "filters": null,
"error_job_count": 1, "trigger": {"kind": "Manual", "schedule_param": null},
"deleted": 0
},
{
"id": 3,
"project_id": 1,
"project_name": "library",
"target_id": 2,
"target_name": "target_03",
"name": "sync_03",
"enabled": 0,
"description": "",
"cron_str": "",
"error_job_count": 0,
"deleted": 0
}
];
let mockRule: ReplicationRule = {
"id": 1,
"project_id": 1,
"project_name": "library",
"target_id": 1,
"target_name": "target_01",
"name": "sync_01",
"enabled": 0,
"description": "",
"cron_str": "",
"error_job_count": 2, "error_job_count": 2,
"deleted": 0 "replicate_deletion": false,
}; "replicate_existing_image_now": false,
},
];
let fixture: ComponentFixture<ListReplicationRuleComponent>; let fixture: ComponentFixture<ListReplicationRuleComponent>;

View File

@ -60,9 +60,8 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
@Input() isSystemAdmin: boolean; @Input() isSystemAdmin: boolean;
@Input() selectedId: number | string; @Input() selectedId: number | string;
@Input() withReplicationJob: boolean; @Input() withReplicationJob: boolean;
@Input() readonly: boolean;
@Input() loading: boolean = false; @Input() loading = false;
@Output() reload = new EventEmitter<boolean>(); @Output() reload = new EventEmitter<boolean>();
@Output() selectOne = new EventEmitter<ReplicationRule>(); @Output() selectOne = new EventEmitter<ReplicationRule>();
@ -100,10 +99,6 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
setInterval(() => ref.markForCheck(), 500); setInterval(() => ref.markForCheck(), 500);
} }
public get opereateAvailable(): boolean {
return !this.readonly && !this.projectId ? true : false;
}
trancatedDescription(desc: string): string { trancatedDescription(desc: string): string {
if (desc.length > 35 ) { if (desc.length > 35 ) {
return desc.substr(0, 35); return desc.substr(0, 35);
@ -135,7 +130,7 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
retrieveRules(ruleName: string = ''): void { retrieveRules(ruleName: string = ''): void {
this.loading = true; this.loading = true;
this.selectedRow = null; /*this.selectedRow = null;*/
toPromise<ReplicationRule[]>(this.replicationService toPromise<ReplicationRule[]>(this.replicationService
.getReplicationRules(this.projectId, ruleName)) .getReplicationRules(this.projectId, ruleName))
.then(rules => { .then(rules => {
@ -156,36 +151,6 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
}); });
} }
filterRuleStatus(status: string) {
if (status === 'all') {
this.changedRules = this.rules;
} else {
this.changedRules = this.rules.filter(policy => policy.enabled === +status);
}
}
toggleConfirm(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TOGGLE_CONFIRM &&
message.state === ConfirmationState.CONFIRMED) {
this.batchDelectionInfos = [];
let rule: ReplicationRule = message.data;
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = rule.name;
this.batchDelectionInfos.push(initBatchMessage);
if (rule) {
rule.enabled = rule.enabled === 0 ? 1 : 0;
toPromise<any>(this.replicationService
.enableReplicationRule(rule.id || '', rule.enabled))
.then(() =>
this.translateService.get('REPLICATION.TOGGLED_SUCCESS')
.subscribe(res => this.batchDelectionInfos[0].status = res))
.catch(error => this.batchDelectionInfos[0].status = error);
}
}
}
replicateRule(rules: ReplicationRule[]): void { replicateRule(rules: ReplicationRule[]): void {
this.replicateManual.emit(rules); this.replicateManual.emit(rules);
} }
@ -216,17 +181,6 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
this.editOne.emit(rule); this.editOne.emit(rule);
} }
toggleRule(rule: ReplicationRule) {
let toggleConfirmMessage: ConfirmationMessage = new ConfirmationMessage(
rule.enabled === 1 ? 'REPLICATION.TOGGLE_DISABLE_TITLE' : 'REPLICATION.TOGGLE_ENABLE_TITLE',
rule.enabled === 1 ? 'REPLICATION.CONFIRM_TOGGLE_DISABLE_POLICY' : 'REPLICATION.CONFIRM_TOGGLE_ENABLE_POLICY',
rule.name || '',
rule,
ConfirmationTargets.TOGGLE_CONFIRM
);
this.toggleConfirmDialog.open(toggleConfirmMessage);
}
jobList(id: string | number): Promise<void> { jobList(id: string | number): Promise<void> {
let ruleData: ReplicationJobItem[]; let ruleData: ReplicationJobItem[];
this.canDeleteRule = true; this.canDeleteRule = true;

View File

@ -1,18 +1,18 @@
export class Project { export class Project {
project_id: number; project_id: number;
owner_id: number; owner_id?: number;
name: string; name: string;
creation_time: Date | string; creation_time?: Date | string;
deleted: number; deleted?: number;
owner_name: string; owner_name?: string;
togglable: boolean; togglable?: boolean;
update_time: Date | string; update_time?: Date | string;
current_user_role_id: number; current_user_role_id?: number;
repo_count: number; repo_count?: number;
has_project_admin_role: boolean; has_project_admin_role?: boolean;
is_member: boolean; is_member?: boolean;
role_name: string; role_name?: string;
metadata: { metadata?: {
public: string | boolean; public: string | boolean;
enable_content_trust: string | boolean; enable_content_trust: string | boolean;
prevent_vul: string | boolean; prevent_vul: string | boolean;

View File

@ -11,7 +11,7 @@ export const REPLICATION_TEMPLATE: string = `
</div> </div>
</div> </div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<hbr-list-replication-rule #listReplicationRule [readonly]="readonly" [projectId]="projectId" [isSystemAdmin]="isSystemAdmin" (replicateManual)=replicateManualRule($event) (selectOne)="selectOneRule($event)" (hideJobs)="hideJobs()" (openNewRule)="openModal()" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)"></hbr-list-replication-rule> <hbr-list-replication-rule #listReplicationRule [projectId]="projectId" [isSystemAdmin]="isSystemAdmin" (replicateManual)=replicateManualRule($event) (selectOne)="selectOneRule($event)" (hideJobs)="hideJobs()" (openNewRule)="openModal()" (editOne)="openEditRule($event)" (reload)="reloadRules($event)" [loading]="loading" [withReplicationJob]="withReplicationJob" (redirect)="customRedirect($event)"></hbr-list-replication-rule>
</div> </div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="padding-left:0px;"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="padding-left:0px;">
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
@ -73,5 +73,6 @@ export const REPLICATION_TEMPLATE: string = `
</div> </div>
</div> </div>
<job-log-viewer #replicationLogViewer></job-log-viewer> <job-log-viewer #replicationLogViewer></job-log-viewer>
<hbr-create-edit-rule [projectId]="projectId" [projectName]="projectName" (goToRegistry)="goRegistry()" (reload)="reloadRules($event)"></hbr-create-edit-rule>
<confirmation-dialog #replicationConfirmDialog [batchInfors]="batchDelectionInfos" (confirmAction)="confirmReplication($event)"></confirmation-dialog> <confirmation-dialog #replicationConfirmDialog [batchInfors]="batchDelectionInfos" (confirmAction)="confirmReplication($event)"></confirmation-dialog>
</div>`; </div>`;

View File

@ -12,7 +12,7 @@ import { DatePickerComponent } from '../datetime-picker/datetime-picker.componen
import { DateValidatorDirective } from '../datetime-picker/date-validator.directive'; import { DateValidatorDirective } from '../datetime-picker/date-validator.directive';
import { FilterComponent } from '../filter/filter.component'; import { FilterComponent } from '../filter/filter.component';
import { InlineAlertComponent } from '../inline-alert/inline-alert.component'; import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
import { ReplicationRule, ReplicationJob, Endpoint } from '../service/interface'; import {ReplicationRule, ReplicationJob, Endpoint} from '../service/interface';
import { ErrorHandler } from '../error-handler/error-handler'; import { ErrorHandler } from '../error-handler/error-handler';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
@ -20,48 +20,91 @@ import { ReplicationService, ReplicationDefaultService } from '../service/replic
import { EndpointService, EndpointDefaultService } from '../service/endpoint.service'; import { EndpointService, EndpointDefaultService } from '../service/endpoint.service';
import { JobLogViewerComponent } from '../job-log-viewer/job-log-viewer.component'; import { JobLogViewerComponent } from '../job-log-viewer/job-log-viewer.component';
import { JobLogService, JobLogDefaultService, ReplicationJobItem } from '../service/index'; import { JobLogService, JobLogDefaultService, ReplicationJobItem } from '../service/index';
import {Project} from "../project-policy-config/project";
import {ProjectDefaultService, ProjectService} from "service/project.service";
describe('Replication Component (inline template)', ()=>{ describe('Replication Component (inline template)', () => {
let mockRules: ReplicationRule[] = [ let mockRules: ReplicationRule[] = [
{ {
"id": 1, "id": 1,
"project_id": 1, "projects": [{
"project_name": "library", "project_id": 33,
"target_id": 1, "owner_id": 1,
"target_name": "target_01", "name": "aeas",
"deleted": 0,
"togglable": false,
"current_user_role_id": 0,
"repo_count": 0,
"metadata": {
"public": false,
"enable_content_trust": "",
"prevent_vul": "",
"severity": "",
"auto_scan": ""},
"owner_name": "",
"creation_time": null,
"update_time": null,
"has_project_admin_role": true,
"is_member": true,
"role_name": ""
}],
"targets": [{
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"insecure": false,
"type": 0
}],
"name": "sync_01", "name": "sync_01",
"enabled": 0,
"description": "", "description": "",
"cron_str": "", "filters": null,
"trigger": {"kind": "Manual", "schedule_param": null},
"error_job_count": 2, "error_job_count": 2,
"deleted": 0 "replicate_deletion": false,
"replicate_existing_image_now": false,
}, },
{ {
"id": 2, "id": 2,
"project_id": 1, "projects": [{
"project_name": "library", "project_id": 33,
"target_id": 3, "owner_id": 1,
"target_name": "target_02", "name": "aeas",
"deleted": 0,
"togglable": false,
"current_user_role_id": 0,
"repo_count": 0,
"metadata": {
"public": false,
"enable_content_trust": "",
"prevent_vul": "",
"severity": "",
"auto_scan": ""},
"owner_name": "",
"creation_time": null,
"update_time": null,
"has_project_admin_role": true,
"is_member": true,
"role_name": ""
}],
"targets": [{
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"insecure": false,
"type": 0
}],
"name": "sync_02", "name": "sync_02",
"enabled": 1,
"description": "", "description": "",
"cron_str": "", "filters": null,
"error_job_count": 1, "trigger": {"kind": "Manual", "schedule_param": null},
"deleted": 0 "error_job_count": 2,
}, "replicate_deletion": false,
{ "replicate_existing_image_now": false,
"id": 3,
"project_id": 1,
"project_name": "library",
"target_id": 2,
"target_name": "target_03",
"name": "sync_03",
"enabled": 0,
"description": "",
"cron_str": "",
"error_job_count": 0,
"deleted": 0
} }
]; ];
@ -95,11 +138,6 @@ describe('Replication Component (inline template)', ()=>{
} }
]; ];
let mockJob: ReplicationJob = {
metadata: {xTotalCount: 3},
data: mockJobs
};
let mockEndpoints: Endpoint[] = [ let mockEndpoints: Endpoint[] = [
{ {
"id": 1, "id": 1,
@ -119,47 +157,47 @@ describe('Replication Component (inline template)', ()=>{
"insecure": false, "insecure": false,
"type": 0 "type": 0
}, },
{
"id": 3,
"endpoint": "https://101.1.11.111",
"name": "target_03",
"username": "admin",
"password": "",
"insecure": false,
"type": 0
},
{
"id": 4,
"endpoint": "http://4.4.4.4",
"name": "target_04",
"username": "",
"password": "",
"insecure": false,
"type": 0
}
]; ];
let mockRule: ReplicationRule = { let mockProjects: Project[] = [
"id": 1, { "project_id": 1,
"project_id": 1, "owner_id": 0,
"project_name": "library", "name": 'project_01',
"target_id": 1, "creation_time": '',
"target_name": "target_01", "deleted": 0,
"name": "sync_01", "owner_name": '',
"enabled": 0, "togglable": false,
"description": "", "update_time": '',
"cron_str": "", "current_user_role_id": 0,
"error_job_count": 2, "repo_count": 0,
"deleted": 0 "has_project_admin_role": false,
"is_member": false,
"role_name": '',
"metadata": {
"public": '',
"enable_content_trust": '',
"prevent_vul": '',
"severity": '',
"auto_scan": '',
}
}];
let mockJob: ReplicationJob = {
metadata: {xTotalCount: 3},
data: mockJobs
}; };
let fixture: ComponentFixture<ReplicationComponent>; let fixture: ComponentFixture<ReplicationComponent>;
let fixtureCreate: ComponentFixture<CreateEditRuleComponent>;
let comp: ReplicationComponent; let comp: ReplicationComponent;
let compCreate: CreateEditRuleComponent;
let replicationService: ReplicationService; let replicationService: ReplicationService;
let endpointService: EndpointService;
let spyRules: jasmine.Spy; let spyRules: jasmine.Spy;
let spyJobs: jasmine.Spy; let spyJobs: jasmine.Spy;
let spyEndpoint: jasmine.Spy;
let deGrids: DebugElement[]; let deGrids: DebugElement[];
let deRules: DebugElement; let deRules: DebugElement;
@ -194,23 +232,29 @@ describe('Replication Component (inline template)', ()=>{
{ provide: SERVICE_CONFIG, useValue: config }, { provide: SERVICE_CONFIG, useValue: config },
{ provide: ReplicationService, useClass: ReplicationDefaultService }, { provide: ReplicationService, useClass: ReplicationDefaultService },
{ provide: EndpointService, useClass: EndpointDefaultService }, { provide: EndpointService, useClass: EndpointDefaultService },
{ provide: ProjectService, useClass: ProjectDefaultService },
{ provide: JobLogService, useClass: JobLogDefaultService } { provide: JobLogService, useClass: JobLogDefaultService }
] ]
}); });
})); }));
beforeEach(()=>{ beforeEach(() => {
fixture = TestBed.createComponent(ReplicationComponent); fixture = TestBed.createComponent(ReplicationComponent);
fixtureCreate = TestBed.createComponent(CreateEditRuleComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compCreate = fixtureCreate.componentInstance;
comp.projectId = 1; comp.projectId = 1;
comp.search.ruleId = 1; comp.search.ruleId = 1;
replicationService = fixture.debugElement.injector.get(ReplicationService); replicationService = fixture.debugElement.injector.get(ReplicationService);
endpointService = fixtureCreate.debugElement.injector.get(EndpointService);
spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(Promise.resolve(mockRules)); spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(Promise.resolve(mockRules));
spyJobs = spyOn(replicationService, 'getJobs').and.returnValues(Promise.resolve(mockJob)); spyJobs = spyOn(replicationService, 'getJobs').and.returnValues(Promise.resolve(mockJob));
spyEndpoint = spyOn(endpointService, 'getEndpoints').and.returnValues(Promise.resolve(mockEndpoints));
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(()=>{ fixture.whenStable().then(()=>{
fixture.detectChanges(); fixture.detectChanges();
@ -221,6 +265,7 @@ describe('Replication Component (inline template)', ()=>{
}); });
}); });
it('Should load replication rules', async(()=>{ it('Should load replication rules', async(()=>{
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(()=>{ fixture.whenStable().then(()=>{

View File

@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, ViewChild, Input, Output, OnDestroy, EventEmitter } from '@angular/core'; import { Component, OnInit, ViewChild, Input, Output, OnDestroy, EventEmitter } from '@angular/core';
import { ResponseOptions, RequestOptions } from '@angular/http';
import { NgModel } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -23,7 +21,7 @@ import { ErrorHandler } from '../error-handler/error-handler';
import { ReplicationService } from '../service/replication.service'; import { ReplicationService } from '../service/replication.service';
import { RequestQueryParams } from '../service/RequestQueryParams'; import { RequestQueryParams } from '../service/RequestQueryParams';
import { ReplicationRule, ReplicationJob, Endpoint, ReplicationJobItem } from '../service/interface'; import { ReplicationRule, ReplicationJob, ReplicationJobItem } from '../service/interface';
import { import {
toPromise, toPromise,
@ -89,13 +87,14 @@ export class SearchOption {
export class ReplicationComponent implements OnInit, OnDestroy { export class ReplicationComponent implements OnInit, OnDestroy {
@Input() projectId: number | string; @Input() projectId: number | string;
@Input() projectName: string;
@Input() isSystemAdmin: boolean; @Input() isSystemAdmin: boolean;
@Input() withReplicationJob: boolean; @Input() withReplicationJob: boolean;
@Input() readonly: boolean;
@Output() redirect = new EventEmitter<ReplicationRule>(); @Output() redirect = new EventEmitter<ReplicationRule>();
@Output() openCreateRule = new EventEmitter<any>(); @Output() openCreateRule = new EventEmitter<any>();
@Output() openEdit = new EventEmitter<string | number>(); @Output() openEdit = new EventEmitter<string | number>();
@Output() goToRegistry = new EventEmitter<any>();
search: SearchOption = new SearchOption(); search: SearchOption = new SearchOption();
@ -106,7 +105,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
currentJobStatus: { key: string, description: string }; currentJobStatus: { key: string, description: string };
changedRules: ReplicationRule[]; changedRules: ReplicationRule[];
initSelectedId: number | string;
rules: ReplicationRule[]; rules: ReplicationRule[];
loading: boolean; loading: boolean;
@ -121,8 +119,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
@ViewChild(ListReplicationRuleComponent) @ViewChild(ListReplicationRuleComponent)
listReplicationRule: ListReplicationRuleComponent; listReplicationRule: ListReplicationRuleComponent;
/* @ViewChild(CreateEditRuleComponent) @ViewChild(CreateEditRuleComponent)
createEditPolicyComponent: CreateEditRuleComponent;*/ createEditPolicyComponent: CreateEditRuleComponent;
@ViewChild("replicationLogViewer") @ViewChild("replicationLogViewer")
replicationLogViewer: JobLogViewerComponent; replicationLogViewer: JobLogViewerComponent;
@ -164,18 +162,20 @@ export class ReplicationComponent implements OnInit, OnDestroy {
} }
} }
// open replication rule
openModal(): void { openModal(): void {
this.openCreateRule.emit(); this.createEditPolicyComponent.openCreateEditRule();
} }
// edit replication rule
openEditRule(rule: ReplicationRule) { openEditRule(rule: ReplicationRule) {
if (rule) { if (rule) {
let editable = true; this.createEditPolicyComponent.openCreateEditRule(rule.id);
if (rule.enabled === 1) {
editable = false;
} }
this.openEdit.emit(rule.id);
} }
goRegistry(): void {
this.goToRegistry.emit();
} }
//Server driven data loading //Server driven data loading
@ -209,6 +209,12 @@ export class ReplicationComponent implements OnInit, OnDestroy {
} }
this.jobsLoading = true; this.jobsLoading = true;
//Do filtering and sorting
this.jobs = doFiltering<ReplicationJobItem>(this.jobs, state);
this.jobs = doSorting<ReplicationJobItem>(this.jobs, state);
this.jobsLoading = false;
toPromise<ReplicationJob>(this.replicationService toPromise<ReplicationJob>(this.replicationService
.getJobs(this.search.ruleId, params)) .getJobs(this.search.ruleId, params))
.then( .then(
@ -382,6 +388,9 @@ export class ReplicationComponent implements OnInit, OnDestroy {
refreshJobs() { refreshJobs() {
this.currentJobStatus = this.jobStatus[0];
this.search.startTime = ' ';
this.search.endTime = ' ';
this.search.repoName = ""; this.search.repoName = "";
this.search.startTimestamp = ""; this.search.startTimestamp = "";
this.search.endTimestamp = ""; this.search.endTimestamp = "";

View File

@ -1,3 +1,4 @@
import {Project} from "../project-policy-config/project";
/** /**
* The base interface contains the general properties * The base interface contains the general properties
* *
@ -83,18 +84,40 @@ export interface Endpoint extends Base {
* *
* @export * @export
* @interface ReplicationRule * @interface ReplicationRule
* @interface Filter
* @interface Trigger
*/ */
export interface ReplicationRule extends Base { export interface ReplicationRule extends Base {
project_id: number | string; [key: string]: any;
project_name: string; id?: number;
target_id: number | string; name: string;
target_name: string; description: string;
enabled: number; projects: Project[];
description?: string; targets: Endpoint[] ;
cron_str?: string; trigger: Trigger ;
start_time?: Date; filters: Filter[] ;
error_job_count?: number; replicate_existing_image_now?: boolean;
deleted: number; replicate_deletion?: boolean;
}
export class Filter {
kind: string;
pattern: string;
constructor(kind: string, pattern: string) {
this.kind = kind;
this.pattern = pattern;
}
}
export class Trigger {
kind: string;
schedule_param: any | {
[key: string]: any | any[];
};
constructor(kind: string, param: any | { [key: string]: any | any[]; }) {
this.kind = kind;
this.schedule_param = param;
}
} }
/** /**
@ -115,7 +138,7 @@ export interface ReplicationJob {
* @interface ReplicationJob * @interface ReplicationJob
*/ */
export interface ReplicationJobItem extends Base { export interface ReplicationJobItem extends Base {
[key: string]: any | any[] [key: string]: any | any[];
status: string; status: string;
repository: string; repository: string;
policy_id: number; policy_id: number;
@ -151,7 +174,7 @@ export interface AccessLog {
* @interface AccessLogItem * @interface AccessLogItem
*/ */
export interface AccessLogItem { export interface AccessLogItem {
[key: string]: any | any[] [key: string]: any | any[];
log_id: number; log_id: number;
project_id: number; project_id: number;
repo_name: string; repo_name: string;

View File

@ -6,7 +6,8 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { Project } from '../project-policy-config/project'; import { Project } from '../project-policy-config/project';
import { ProjectPolicy } from '../project-policy-config/project-policy-config.component'; import { ProjectPolicy } from '../project-policy-config/project-policy-config.component';
import {HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS} from "../utils"; import {HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS, buildHttpRequestOptions} from "../utils";
import {RequestQueryParams} from "./RequestQueryParams";
/** /**
* Define the service methods to handle the Prject related things. * Define the service methods to handle the Prject related things.
@ -38,6 +39,8 @@ export abstract class ProjectService {
* @memberOf EndpointService * @memberOf EndpointService
*/ */
abstract updateProjectPolicy(projectId: number | string, projectPolicy: ProjectPolicy): Observable<any> | Promise<any> | any; abstract updateProjectPolicy(projectId: number | string, projectPolicy: ProjectPolicy): Observable<any> | Promise<any> | any;
abstract listProjects(name: string, isPublic: number, page?: number, pageSize?: number): Observable<Project[]> | Promise<Project[]> | Project[];
} }
/** /**
@ -68,6 +71,27 @@ export class ProjectDefaultService extends ProjectService {
.catch(error => Observable.throw(error)); .catch(error => Observable.throw(error));
} }
listProjects(name: string, isPublic: number, page?: number, pageSize?: number): Observable<Project[]> | Promise<Project[]> | Project[] {
let params = new RequestQueryParams();
if (page && pageSize) {
params.set('page', page + '');
params.set('page_size', pageSize + '');
}
if (name && name.trim() !== "") {
params.set('name', name);
}
if (isPublic !== undefined) {
params.set('public', '' + isPublic);
}
// let options = new RequestOptions({ headers: this.getHeaders, search: params });
return this.http
.get(`/api/projects`, buildHttpRequestOptions(params))
.map(response => response.json())
.catch(error => Observable.throw(error));
}
public updateProjectPolicy(projectId: number | string, projectPolicy: ProjectPolicy): any { public updateProjectPolicy(projectId: number | string, projectPolicy: ProjectPolicy): any {
return this.http return this.http
.put(`/api/projects/${projectId}`, { 'metadata': { .put(`/api/projects/${projectId}`, { 'metadata': {

View File

@ -1,6 +1,6 @@
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { RequestQueryParams } from './RequestQueryParams'; import { RequestQueryParams } from './RequestQueryParams';
import { ReplicationJob, ReplicationRule, ReplicationJobItem } from './interface'; import {ReplicationJob, ReplicationRule, ReplicationJobItem} from './interface';
import { Injectable, Inject } from "@angular/core"; import { Injectable, Inject } from "@angular/core";
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
import { Http, RequestOptions } from '@angular/http'; import { Http, RequestOptions } from '@angular/http';
@ -62,7 +62,7 @@ export abstract class ReplicationService {
* *
* @memberOf ReplicationService * @memberOf ReplicationService
*/ */
abstract updateReplicationRule(replicationRule: ReplicationRule): Observable<any> | Promise<any> | any; abstract updateReplicationRule(id: number, rep: ReplicationRule): Observable<any> | Promise<any> | any;
/** /**
* Delete the specified replication rule. * Delete the specified replication rule.
@ -159,7 +159,7 @@ export class ReplicationDefaultService extends ReplicationService {
//Private methods //Private methods
//Check if the rule object is valid //Check if the rule object is valid
_isValidRule(rule: ReplicationRule): boolean { _isValidRule(rule: ReplicationRule): boolean {
return rule !== undefined && rule != null && rule.name !== undefined && rule.name.trim() !== '' && rule.target_id !== 0; return rule !== undefined && rule != null && rule.name !== undefined && rule.name.trim() !== '' && rule.targets.length !== 0;
} }
public getReplicationRules(projectId?: number | string, ruleName?: string, queryParams?: RequestQueryParams): Observable<ReplicationRule[]> | Promise<ReplicationRule[]> | ReplicationRule[] { public getReplicationRules(projectId?: number | string, ruleName?: string, queryParams?: RequestQueryParams): Observable<ReplicationRule[]> | Promise<ReplicationRule[]> | ReplicationRule[] {
@ -177,7 +177,7 @@ export class ReplicationDefaultService extends ReplicationService {
return this.http.get(this._ruleBaseUrl, buildHttpRequestOptions(queryParams)).toPromise() return this.http.get(this._ruleBaseUrl, buildHttpRequestOptions(queryParams)).toPromise()
.then(response => response.json() as ReplicationRule[]) .then(response => response.json() as ReplicationRule[])
.catch(error => Promise.reject(error)) .catch(error => Promise.reject(error));
} }
public getReplicationRule(ruleId: number | string): Observable<ReplicationRule> | Promise<ReplicationRule> | ReplicationRule { public getReplicationRule(ruleId: number | string): Observable<ReplicationRule> | Promise<ReplicationRule> | ReplicationRule {
@ -201,13 +201,13 @@ export class ReplicationDefaultService extends ReplicationService {
.catch(error => Promise.reject(error)); .catch(error => Promise.reject(error));
} }
public updateReplicationRule(replicationRule: ReplicationRule): Observable<any> | Promise<any> | any { public updateReplicationRule(id: number, rep: ReplicationRule): Observable<any> | Promise<any> | any {
if (!this._isValidRule(replicationRule) || !replicationRule.id) { if (!this._isValidRule(rep)) {
return Promise.reject('Bad argument'); return Promise.reject('Bad argument');
} }
let url: string = `${this._ruleBaseUrl}/${replicationRule.id}`; let url = `${this._ruleBaseUrl}/${id}`;
return this.http.put(url, JSON.stringify(replicationRule), HTTP_JSON_OPTIONS).toPromise() return this.http.put(url, JSON.stringify(rep), HTTP_JSON_OPTIONS).toPromise()
.then(response => response) .then(response => response)
.catch(error => Promise.reject(error)); .catch(error => Promise.reject(error));
} }
@ -298,7 +298,7 @@ export class ReplicationDefaultService extends ReplicationService {
return Promise.reject('Bad argument'); return Promise.reject('Bad argument');
} }
let logUrl: string = `${this._jobBaseUrl}/${jobId}/log`; let logUrl = `${this._jobBaseUrl}/${jobId}/log`;
return this.http.get(logUrl, HTTP_GET_OPTIONS).toPromise() return this.http.get(logUrl, HTTP_GET_OPTIONS).toPromise()
.then(response => response.text()) .then(response => response.text())
.catch(error => Promise.reject(error)); .catch(error => Promise.reject(error));

View File

@ -2,8 +2,8 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HttpModule, Http } from '@angular/http'; import { HttpModule, Http } from '@angular/http';
import { ClarityModule } from 'clarity-angular'; import { ClarityModule } from 'clarity-angular';
import { FormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from '@ngx-translate/core'; import { TranslateModule, TranslateLoader, MissingTranslationHandler } from '@ngx-translate/core';
import { MyMissingTranslationHandler } from '../i18n/missing-trans.handler'; import { MyMissingTranslationHandler } from '../i18n/missing-trans.handler';
import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TranslatorJsonLoader } from '../i18n/local-json.loader'; import { TranslatorJsonLoader } from '../i18n/local-json.loader';
@ -41,6 +41,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
CommonModule, CommonModule,
HttpModule, HttpModule,
FormsModule, FormsModule,
ReactiveFormsModule,
ClipboardModule, ClipboardModule,
CookieModule.forRoot(), CookieModule.forRoot(),
ClarityModule.forRoot(), ClarityModule.forRoot(),
@ -60,6 +61,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
CommonModule, CommonModule,
HttpModule, HttpModule,
FormsModule, FormsModule,
ReactiveFormsModule,
CookieModule, CookieModule,
ClipboardModule, ClipboardModule,
ClarityModule, ClarityModule,

View File

@ -31,7 +31,7 @@
"clarity-icons": "^0.10.17", "clarity-icons": "^0.10.17",
"clarity-ui": "^0.10.17", "clarity-ui": "^0.10.17",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"harbor-ui": "0.6.45", "harbor-ui": "0.6.47",
"intl": "^1.2.5", "intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2", "mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0", "ngx-cookie": "^1.0.0",

View File

@ -50,8 +50,6 @@ import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deac
import { MemberGuard } from './shared/route/member-guard-activate.service'; import { MemberGuard } from './shared/route/member-guard-activate.service';
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component'; import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
import { ReplicationRuleComponent} from "./replication/replication-rule/replication-rule.component";
import {LeavingNewRuleRouteDeactivate} from "./shared/route/leaving-new-rule-deactivate.service";
import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-repository-deactivate.service'; import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-repository-deactivate.service';
const harborRoutes: Routes = [ const harborRoutes: Routes = [
@ -92,20 +90,6 @@ const harborRoutes: Routes = [
canActivate: [SystemAdminGuard], canActivate: [SystemAdminGuard],
canActivateChild: [SystemAdminGuard], canActivateChild: [SystemAdminGuard],
}, },
{
path: 'replications/:id/rule',
component: ReplicationRuleComponent,
canActivate: [SystemAdminGuard],
canActivateChild: [SystemAdminGuard],
canDeactivate: [LeavingNewRuleRouteDeactivate]
},
{
path: 'replications/new-rule',
component: ReplicationRuleComponent,
canActivate: [SystemAdminGuard],
canActivateChild: [SystemAdminGuard],
canDeactivate: [LeavingNewRuleRouteDeactivate]
},
{ {
path: 'tags/:id/:repo', path: 'tags/:id/:repo',
component: TagRepositoryComponent, component: TagRepositoryComponent,

View File

@ -13,6 +13,7 @@
pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$"
minlength="2" minlength="2"
#projectName="ngModel" #projectName="ngModel"
autocomplete="off"
(keyup)='handleValidation()'> (keyup)='handleValidation()'>
<span class="tooltip-content"> <span class="tooltip-content">
{{ nameTooltipText | translate }} {{ nameTooltipText | translate }}

View File

@ -74,7 +74,7 @@ export class CreateProjectComponent implements AfterViewChecked, OnInit, OnDestr
ngOnInit(): void { ngOnInit(): void {
this.proNameChecker this.proNameChecker
.debounceTime(500) .debounceTime(500)
.distinctUntilChanged() //.distinctUntilChanged()
.subscribe((name: string) => { .subscribe((name: string) => {
let cont = this.currentForm.controls["create_project_name"]; let cont = this.currentForm.controls["create_project_name"];
if (cont && this.hasChanged) { if (cont && this.hasChanged) {
@ -166,6 +166,8 @@ export class CreateProjectComponent implements AfterViewChecked, OnInit, OnDestr
newProject() { newProject() {
this.project = new Project(); this.project = new Project();
this.hasChanged = false; this.hasChanged = false;
this.isNameValid = true;
this.createProjectOpened = true; this.createProjectOpened = true;
} }

Some files were not shown because too many files have changed in this diff Show More