mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 10:45:45 +01:00
Merge branch 'job_service' of https://github.com/vmware/harbor into job_service
This commit is contained in:
commit
b001180d26
@ -2,12 +2,13 @@ workspace:
|
||||
base: /drone
|
||||
path: src/github.com/vmware/harbor
|
||||
|
||||
pipeline:
|
||||
clone:
|
||||
clone:
|
||||
git:
|
||||
image: plugins/git
|
||||
tags: true
|
||||
recursive: false
|
||||
|
||||
pipeline:
|
||||
check-org-membership:
|
||||
image: 'wdc-harbor-ci.eng.vmware.com/default-project/vic-integration-test:1.44'
|
||||
pull: true
|
||||
@ -17,9 +18,11 @@ pipeline:
|
||||
SHELL: /bin/bash
|
||||
secrets:
|
||||
- github_automation_api_key
|
||||
- skip_check_membership
|
||||
commands:
|
||||
- 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:
|
||||
status: success
|
||||
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -31,6 +31,8 @@ src/ui_ng/typings/
|
||||
**/*yarn-error.log.*
|
||||
.idea/
|
||||
.DS_Store
|
||||
.project
|
||||
.vscode/
|
||||
**/node_modules
|
||||
**/ssl/
|
||||
**/proxy.config.json
|
||||
|
@ -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.
|
||||
|
||||
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
|
||||
|
||||
You can propose new designs for existing Harbor features. You can also design
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: harbor
|
||||
version: 0.0.1
|
||||
appVersion: 1.3.0
|
||||
description: An Enterprise-class Docker Registry Harbor by VMware
|
||||
version: 0.1.0
|
||||
appVersion: 1.4.0
|
||||
description: An Enterprise-class Docker Registry by VMware
|
||||
keywords:
|
||||
- vmware
|
||||
- docker
|
||||
@ -10,8 +10,10 @@ keywords:
|
||||
home: https://github.com/vmware/harbor
|
||||
icon: https://github.com/vmware/harbor/blob/master/docs/img/harbor_logo.png
|
||||
sources:
|
||||
- https://github.com/vmware/harbor
|
||||
- https://github.com/vmware/harbor/tree/master/contrib/helm/harbor
|
||||
maintainers:
|
||||
- name: Jesse Hu
|
||||
email: huh@vmware.com
|
||||
- name: paulczar
|
||||
email: username.taken@gmail.com
|
||||
engine: gotpl
|
||||
|
@ -1,27 +1,76 @@
|
||||
# Project Harbor by VMware
|
||||
|
||||
[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.
|
||||
# Helm Chart for Harbor
|
||||
|
||||
## 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
|
||||
|
||||
- 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
|
||||
|
||||
## 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
|
||||
|
||||
To install the chart with the release name `my-release`:
|
||||
|
||||
First install [Helm CLI](https://github.com/kubernetes/helm#install), then initialize Helm.
|
||||
```bash
|
||||
$ git clone https://github.com/vmware/harbor.git
|
||||
$ cd harbor/contrib/helm/harbor
|
||||
$ helm install --name my-release incubator/harbor
|
||||
helm init --canary-image
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
@ -30,26 +79,29 @@ The command deploys Harbor on the Kubernetes cluster in the default configuratio
|
||||
To uninstall/delete the `my-release` deployment:
|
||||
|
||||
```bash
|
||||
$ helm delete my-release
|
||||
helm delete my-release
|
||||
```
|
||||
|
||||
The command removes all the Kubernetes components associated with the chart and deletes the release.
|
||||
|
||||
## 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 |
|
||||
| ----------------------- | ---------------------------------- | ----------------------- |
|
||||
| **Harbor** |
|
||||
| `externalDomain` | domain harbor will run on (https://*harbor.url*/) |`harbor.192.168.99.100.xip.io` |
|
||||
| `tls_crt` | TLS certificate to use for Harbor's https endpoint | see values.yaml |
|
||||
| `tls_key` | TLS key to use for Harbor's https endpoint | see values.yaml |
|
||||
| `ca_crt` | CA Cert for self signed TLS cert | see values.yaml |
|
||||
| `harborImageTag` | The tag for Harbor docker images | `v1.4.0` |
|
||||
| `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` |
|
||||
| `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` |
|
||||
| `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` |
|
||||
| `secretKey` | The secret key used for encryption. Must be a string of 16 chars. | `not-a-secure-key` |
|
||||
| **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.emailHost` | email server | `smtp.mydomain.com` |
|
||||
| `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 |
|
||||
| **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.key` | jobservice key | `not-a-secure-key` |
|
||||
| `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 |
|
||||
| **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.key` | ui key | `not-a-secure-key` |
|
||||
| `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 |
|
||||
| **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.host` | MySQL Server | `~` |
|
||||
| `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.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml |
|
||||
| **Registry** |
|
||||
| `registry.image.repository` | Repository for registry image | `vmware/harbor-registry` |
|
||||
| `registry.image.tag` | Tag for registry image | `v1.3.0` |
|
||||
| `registry.image.repository` | Repository for registry image | `vmware/registry-photon` |
|
||||
| `registry.image.tag` | Tag for registry image | `v2.6.2-v1.4.0` |
|
||||
| `registry.image.pullPolicy` | Pull Policy for registry image | `IfNotPresent` |
|
||||
| `registry.rootCrt` | registry root cert | see values.yaml |
|
||||
| `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.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml |
|
||||
| **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.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 |
|
||||
| | | |
|
||||
|
||||
@ -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:
|
||||
|
||||
```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,
|
||||
|
||||
```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)
|
||||
|
||||
## 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."*
|
||||
|
@ -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 \
|
||||
-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 }}
|
||||
|
@ -17,7 +17,7 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this
|
||||
{{- end -}}
|
||||
|
||||
{{/* Helm required labels */}}
|
||||
{{- define "helm.labels" -}}
|
||||
{{- define "harbor.labels" -}}
|
||||
heritage: {{ .Release.Service }}
|
||||
release: {{ .Release.Name }}
|
||||
chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
@ -25,7 +25,7 @@ app: "{{ template "harbor.name" . }}"
|
||||
{{- end -}}
|
||||
|
||||
{{/* matchLabels */}}
|
||||
{{- define "helm.matchLabels" -}}
|
||||
{{- define "harbor.matchLabels" -}}
|
||||
release: {{ .Release.Name }}
|
||||
app: "{{ template "harbor.name" . }}"
|
||||
{{- end -}}
|
||||
|
@ -3,7 +3,8 @@ kind: ConfigMap
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-adminserver"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: adminserver
|
||||
data:
|
||||
{{ if .Values.mysql.host -}}
|
||||
MYSQL_HOST: "{{ .Values.mysql.host }}"
|
||||
@ -19,21 +20,26 @@ data:
|
||||
EMAIL_SSL: "{{ .Values.adminserver.emailSsl }}"
|
||||
EMAIL_FROM: "{{ .Values.adminserver.emailFrom }}"
|
||||
EMAIL_IDENTITY: "{{ .Values.adminserver.emailIdentity }}"
|
||||
EMAIL_INSECURE: "{{ .Values.adminserver.emailInsecure }}"
|
||||
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"
|
||||
TOKEN_SERVICE_URL: "http://{{ template "harbor.fullname" . }}-ui/service/token"
|
||||
WITH_NOTARY: "{{ .Values.notary.enabled }}"
|
||||
LOG_LEVEL: "info"
|
||||
IMAGE_STORE_PATH: "/"
|
||||
AUTH_MODE: "database"
|
||||
IMAGE_STORE_PATH: "/" # This is a temporary hack.
|
||||
AUTH_MODE: "db_auth"
|
||||
SELF_REGISTRATION: "on"
|
||||
LDAP_URL: "ldaps://ldapserver"
|
||||
LDAP_SEARCH_DN: ""
|
||||
LDAP_BASE_DN: ""
|
||||
LDAP_FILTER: "(objectClass=person)"
|
||||
LDAP_UID: "uid"
|
||||
LDAP_SCOPE: "3"
|
||||
LDAP_SCOPE: "2"
|
||||
LDAP_TIMEOUT: "5"
|
||||
LDAP_TIMEOUT: "5"
|
||||
LDAP_VERIFY_CERT: "True"
|
||||
DATABASE_TYPE: "mysql"
|
||||
PROJECT_CREATION_RESTRICTION: "everyone"
|
||||
VERIFY_REMOTE_CERT: "off"
|
||||
@ -45,3 +51,12 @@ data:
|
||||
RESET: "false"
|
||||
WITH_CLAIR: "{{ .Values.clair.enabled }}"
|
||||
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"
|
||||
|
@ -3,10 +3,11 @@ kind: Secret
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-adminserver"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: adminserver
|
||||
type: Opaque
|
||||
data:
|
||||
key: {{ .Values.adminserver.key | b64enc | quote }}
|
||||
secretKey: {{ .Values.secretKey | b64enc | quote }}
|
||||
EMAIL_PWD: {{ .Values.adminserver.emailPwd | b64enc | quote }}
|
||||
HARBOR_ADMIN_PASSWORD: {{ .Values.adminserver.harborAdminPassword | b64enc | quote }}
|
||||
MYSQL_PWD: {{ .Values.mysql.pass | b64enc | quote }}
|
||||
|
@ -3,20 +3,21 @@ kind: StatefulSet
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-adminserver"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: adminserver
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: "{{ template "harbor.fullname" . }}"
|
||||
serviceName: "{{ template "harbor.fullname" . }}-adminserver"
|
||||
selector:
|
||||
matchLabels:
|
||||
{{ include "helm.matchLabels" . | indent 6 }}
|
||||
{{ include "harbor.matchLabels" . | indent 6 }}
|
||||
component: adminserver
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
{{ include "harbor.labels" . | indent 8 }}
|
||||
component: adminserver
|
||||
component: adminserver
|
||||
spec:
|
||||
containers:
|
||||
- name: adminserver
|
||||
@ -53,7 +54,7 @@ spec:
|
||||
secret:
|
||||
secretName: "{{ template "harbor.fullname" . }}-adminserver"
|
||||
items:
|
||||
- key: key
|
||||
- key: secretKey
|
||||
path: key
|
||||
{{- if .Values.persistence.enabled }}
|
||||
volumeClaimTemplates:
|
||||
|
@ -2,12 +2,10 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-adminserver"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
selector:
|
||||
{{ include "helm.matchLabels" . | indent 4 }}
|
||||
component: adminserver
|
||||
{{ include "harbor.matchLabels" . | indent 4 }}
|
||||
component: adminserver
|
@ -4,7 +4,8 @@ kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ template "harbor.fullname" . }}
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: clair
|
||||
data:
|
||||
config.yaml: |
|
||||
clair:
|
||||
|
@ -4,14 +4,18 @@ kind: Deployment
|
||||
metadata:
|
||||
name: {{ template "harbor.fullname" . }}-clair
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: clair
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{ include "harbor.matchLabels" . | indent 6 }}
|
||||
component: clair
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
{{ include "harbor.labels" . | indent 8 }}
|
||||
component: clair
|
||||
spec:
|
||||
containers:
|
||||
@ -34,4 +38,4 @@ spec:
|
||||
items:
|
||||
- key: config.yaml
|
||||
path: config.yaml
|
||||
{{ end }}
|
||||
{{ end }}
|
@ -1,19 +1,4 @@
|
||||
{{ 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
|
||||
# to get it working for now.
|
||||
# see https://github.com/vmware/harbor/issues/3250
|
||||
@ -21,11 +6,12 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: clair
|
||||
labels:
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 6060
|
||||
selector:
|
||||
app: "{{ template "harbor.fullname" . }}"
|
||||
component: adminserver
|
||||
release: {{ .Release.Name }}
|
||||
{{ end }}
|
||||
{{ include "harbor.matchLabels" . | indent 4 }}
|
||||
component: clair
|
||||
{{ end }}
|
@ -4,8 +4,8 @@ kind: Secret
|
||||
metadata:
|
||||
name: {{ template "harbor.fullname" . }}-clair-pg-config
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
POSTGRES_PASSWORD: {{ .Values.clair.postgresPassword | b64enc | quote }}
|
||||
{{ end }}
|
||||
{{ end }}
|
@ -4,19 +4,19 @@ kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ template "harbor.fullname" . }}-clair-pg
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: clair-pg
|
||||
spec:
|
||||
serviceName: "{{ template "harbor.fullname" . }}-clair-pg"
|
||||
selector:
|
||||
matchLabels:
|
||||
{{ include "helm.matchLabels" . | indent 6 }}
|
||||
{{ include "harbor.matchLabels" . | indent 6 }}
|
||||
component: clair-pg
|
||||
template:
|
||||
metadata:
|
||||
name: {{ template "harbor.fullname" . }}-clair-pg
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
{{ include "harbor.labels" . | indent 8 }}
|
||||
component: clair-pg
|
||||
spec:
|
||||
containers:
|
||||
@ -55,7 +55,7 @@ spec:
|
||||
- metadata:
|
||||
name: pgdata
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
{{ include "harbor.labels" . | indent 8 }}
|
||||
spec:
|
||||
accessModes: [{{ .Values.clair.volumes.pgData.accessMode | quote }}]
|
||||
{{- if .Values.clair.volumes.pgData.storageClass }}
|
||||
@ -69,4 +69,4 @@ spec:
|
||||
requests:
|
||||
storage: {{ .Values.clair.volumes.pgData.size | quote }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
@ -4,27 +4,11 @@ kind: Service
|
||||
metadata:
|
||||
name: {{ template "harbor.fullname" . }}-clair-pg
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 5432
|
||||
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
|
||||
{{ end }}
|
||||
{{ end }}
|
@ -3,16 +3,16 @@ kind: Ingress
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-ingress"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
annotations:
|
||||
ingress.kubernetes.io/ssl-redirect: "true"
|
||||
ingress.kubernetes.io/body-size: "0"
|
||||
ingress.kubernetes.io/proxy-body-size: "0"
|
||||
{{ toYaml .Values.ingress.annotations | indent 4 }}
|
||||
spec:
|
||||
{{ if not .Values.insecureRegistry }}
|
||||
tls:
|
||||
- hosts:
|
||||
- "{{ .Values.externalDomain }}"
|
||||
secretName: "{{ template "harbor.fullname" . }}-ingress"
|
||||
{{ end }}
|
||||
rules:
|
||||
- host: "{{ .Values.externalDomain }}"
|
||||
http:
|
||||
@ -25,7 +25,3 @@ spec:
|
||||
backend:
|
||||
serviceName: {{ template "harbor.fullname" . }}-registry
|
||||
servicePort: 5000
|
||||
- path: /v1
|
||||
backend:
|
||||
serviceName: {{ template "harbor.fullname" . }}-fake-service
|
||||
servicePort: 5000
|
||||
|
@ -1,11 +1,15 @@
|
||||
{{ if not .Values.insecureRegistry }}
|
||||
{{ $ca := genCA "harbor-ca" 365 }}
|
||||
{{ $cert := genSignedCert .Values.externalDomain nil nil 365 $ca }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-ingress"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
type: kubernetes.io/tls
|
||||
data:
|
||||
tls.crt: {{ .Values.tlsCrt | b64enc | quote }}
|
||||
tls.key: {{ .Values.tlsKey | b64enc | quote }}
|
||||
ca.crt: {{ .Values.caCrt | b64enc | quote }}
|
||||
tls.crt: {{ .Values.tlsCrt | default $cert.Cert | b64enc | quote }}
|
||||
tls.key: {{ .Values.tlsKey | default $cert.Key | b64enc | quote }}
|
||||
ca.crt: {{ .Values.caCrt | default $ca.Cert | b64enc | quote }}
|
||||
{{ end }}
|
||||
|
@ -3,7 +3,7 @@ kind: ConfigMap
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-jobservice"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
data:
|
||||
app.conf: |+
|
||||
appname = jobservice
|
||||
|
@ -3,14 +3,18 @@ kind: Deployment
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-jobservice"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: jobservice
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
{{ include "harbor.matchLabels" . | indent 6 }}
|
||||
component: jobservice
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
{{ include "harbor.labels" . | indent 8 }}
|
||||
component: jobservice
|
||||
spec:
|
||||
containers:
|
||||
@ -50,7 +54,7 @@ spec:
|
||||
secret:
|
||||
secretName: "{{ template "harbor.fullname" . }}-jobservice"
|
||||
items:
|
||||
- key: key
|
||||
- key: secretKey
|
||||
path: key
|
||||
- name: job-logs
|
||||
emptyDir: {}
|
||||
|
@ -3,9 +3,9 @@ kind: Secret
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-jobservice"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
secretKey: {{ .Values.secretKey | 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 }}
|
@ -3,11 +3,11 @@ kind: Service
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-jobservice"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
selector:
|
||||
{{ include "helm.matchLabels" . | indent 4 }}
|
||||
{{ include "harbor.matchLabels" . | indent 4 }}
|
||||
component: jobservice
|
||||
|
@ -3,7 +3,7 @@ kind: Secret
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-mysql"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
mysqlRootPassword: {{ .Values.mysql.pass | b64enc | quote }}
|
||||
|
@ -3,19 +3,19 @@ kind: StatefulSet
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-mysql"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: mysql
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: "{{ template "harbor.fullname" . }}-mysql"
|
||||
selector:
|
||||
matchLabels:
|
||||
{{ include "helm.matchLabels" . | indent 6 }}
|
||||
{{ include "harbor.matchLabels" . | indent 6 }}
|
||||
component: mysql
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
{{ include "harbor.labels" . | indent 8 }}
|
||||
component: mysql
|
||||
spec:
|
||||
containers:
|
||||
@ -43,7 +43,7 @@ spec:
|
||||
- metadata:
|
||||
name: "mysql-data"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
{{ include "harbor.labels" . | indent 8 }}
|
||||
spec:
|
||||
accessModes: [{{ .Values.mysql.volumes.data.accessMode | quote }}]
|
||||
{{- if .Values.mysql.volumes.data.storageClass }}
|
||||
|
@ -3,10 +3,10 @@ kind: Service
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-mysql"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 3306
|
||||
selector:
|
||||
{{ include "helm.matchLabels" . | indent 4 }}
|
||||
component: mysql
|
||||
{{ include "harbor.matchLabels" . | indent 4 }}
|
||||
component: mysql
|
@ -3,7 +3,7 @@ kind: ConfigMap
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-registry"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
data:
|
||||
config.yml: |+
|
||||
version: 0.1
|
||||
|
@ -3,8 +3,8 @@ kind: Secret
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-registry"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
httpSecret: {{ .Values.registry.httpSecret | b64enc | quote }}
|
||||
root.crt: {{ .Values.registry.rootCrt | b64enc | quote }}
|
||||
root.crt: {{ .Values.registry.rootCrt | b64enc | quote }}
|
@ -3,19 +3,19 @@ kind: StatefulSet
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-registry"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: registry
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: "{{ template "harbor.fullname" . }}-registry"
|
||||
selector:
|
||||
matchLabels:
|
||||
{{ include "helm.matchLabels" . | indent 6 }}
|
||||
{{ include "harbor.matchLabels" . | indent 6 }}
|
||||
component: registry
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
{{ include "harbor.labels" . | indent 8 }}
|
||||
component: registry
|
||||
spec:
|
||||
containers:
|
||||
@ -61,8 +61,8 @@ spec:
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: "registry-data"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
labels:
|
||||
{{ include "harbor.labels" . | indent 8 }}
|
||||
spec:
|
||||
accessModes: [{{ .Values.registry.volumes.data.accessMode | quote }}]
|
||||
{{- if .Values.registry.volumes.data.storageClass }}
|
||||
@ -76,4 +76,4 @@ spec:
|
||||
requests:
|
||||
storage: {{ .Values.registry.volumes.data.size | quote }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
@ -3,10 +3,10 @@ kind: Service
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-registry"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 5000
|
||||
selector:
|
||||
{{ include "helm.matchLabels" . | indent 4 }}
|
||||
component: registry
|
||||
{{ include "harbor.matchLabels" . | indent 4 }}
|
||||
component: registry
|
@ -3,7 +3,7 @@ kind: ConfigMap
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-ui"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
data:
|
||||
app.conf: |+
|
||||
appname = Harbor
|
||||
|
@ -3,14 +3,14 @@ kind: Deployment
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-ui"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
component: ui
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 8 }}
|
||||
{{ include "harbor.matchLabels" . | indent 8 }}
|
||||
component: ui
|
||||
spec:
|
||||
containers:
|
||||
@ -64,7 +64,7 @@ spec:
|
||||
secret:
|
||||
secretName: "{{ template "harbor.fullname" . }}-ui"
|
||||
items:
|
||||
- key: key
|
||||
- key: secretKey
|
||||
path: key
|
||||
- name: ui-secrets-private-key
|
||||
secret:
|
||||
|
@ -3,10 +3,10 @@ kind: Secret
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-ui"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
secretKey: {{ .Values.secretKey | b64enc | quote }}
|
||||
secret: {{ .Values.ui.secret | b64enc | quote }}
|
||||
key: {{ .Values.ui.key | b64enc | quote }}
|
||||
private_key.pem: {{ .Values.ui.privateKeyPem | b64enc | quote }}
|
||||
jobserviceSecret: {{ .Values.jobservice.secret | b64enc | quote }}
|
||||
jobserviceSecret: {{ .Values.jobservice.secret | b64enc | quote }}
|
@ -3,11 +3,11 @@ kind: Service
|
||||
metadata:
|
||||
name: "{{ template "harbor.fullname" . }}-ui"
|
||||
labels:
|
||||
{{ include "helm.labels" . | indent 4 }}
|
||||
{{ include "harbor.labels" . | indent 4 }}
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
selector:
|
||||
{{ include "helm.matchLabels" . | indent 4 }}
|
||||
{{ include "harbor.matchLabels" . | indent 4 }}
|
||||
component: ui
|
||||
|
@ -1,5 +1,5 @@
|
||||
# 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.
|
||||
## example
|
||||
# mysql:
|
||||
@ -28,81 +28,34 @@
|
||||
persistence:
|
||||
enabled: false
|
||||
|
||||
externalDomain: harbor.192.168.99.100.xip.io
|
||||
## tls_crt, tls_key, ca_crt should match the domain above
|
||||
# The tag for Harbor docker images.
|
||||
harborImageTag: &harbor_image_tag v1.4.0
|
||||
|
||||
tlsCrt: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDJDCCAgygAwIBAgIJAKNSg1jp3l2oMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
|
||||
BAMMB3Rlc3QtY2EwHhcNMTgwMTEzMTg1NTIwWhcNMTgwMzE0MTg1NTIwWjAnMSUw
|
||||
IwYDVQQDDBxoYXJib3IuMTkyLjE2OC45OS4xMDAueGlwLmlvMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxlAD8tlryoGsweXCwDgfyPGkaM9hXsVLW2PH
|
||||
/vGWBVMXOdxpFhuvH7tXmqN3Ek39YQjcsb+nHAGx7ynx6KFtvzcXCjGfeI1yuoN0
|
||||
8H2sfV7yxtkVLu/uJGb8mSfsw9ubOR/zMbrsD1oH0tzi3cnW0kcbY0u0Xp/5g0PP
|
||||
+tig0X+PDfumK/W6KnTOAmnfNTJwhhlljako+lveT5EjVtQMdJmV16PZJwCDA4b9
|
||||
2U8EkLOjXcSg2ad03XxASGUuG8oMLHNXF0zcJ9421DviaRQGJUSjR571t/YCc2KK
|
||||
AQVZ/zSI5duQVysfMZrjiuvSQfKSWRVY6z0JAWH7+Dx+1u8ilwIDAQABo2gwZjAJ
|
||||
BgNVHRMEAjAAMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYB
|
||||
BQUHAwEwLQYDVR0RBCYwJIIcaGFyYm9yLjE5Mi4xNjguOTkuMTAwLnhpcC5pb4cE
|
||||
wKhjZDANBgkqhkiG9w0BAQsFAAOCAQEATgS0Y2wQiCQrVfiDFSIxtIBK2af0qtoA
|
||||
J4DZ/1Jo01uGycFCyt9KOKbmFubrJu9NHuACL9od3RI37k6L73lV2zB3sS4NEcH2
|
||||
SvF+rOE7gmtgJULHCDFEWSMxHdUFwcdG1trRVe+9Gyp/LGdC4yyycmwquz7YXf+r
|
||||
7b5r26rFAYmO8rWYtDt4clC3JSR3O1BmF5ktRNzUtRvrzr3UuwYz0Wy72S/Sa+Iu
|
||||
RnassP8mg6PCppeGccYFcFihL9kDl4g4Xu/PaMiKdxjdeAV6xAd7VbKBZSi/ljnF
|
||||
OUUUi7MDJuUWbHEb0XrEXNzihBzf7bu4I2MftQidIg6LwWjiYZRHmw==
|
||||
-----END CERTIFICATE-----
|
||||
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-----
|
||||
# The FQDN for Harbor service.
|
||||
externalDomain: harbor.my.domain
|
||||
# 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.
|
||||
insecureRegistry: false
|
||||
# The TLS certificate for Harbor. The common name of tlsCrt must match the externalDomain above.
|
||||
tlsCrt:
|
||||
tlsKey:
|
||||
caCrt:
|
||||
|
||||
# The secret key used for encryption. Must be a string of 16 chars.
|
||||
secretKey: not-a-secure-key
|
||||
|
||||
# These annotations allow the registry to work behind the nginx
|
||||
# ingress controller.
|
||||
ingress:
|
||||
annotations:
|
||||
ingress.kubernetes.io/ssl-redirect: "true"
|
||||
ingress.kubernetes.io/body-size: "0"
|
||||
ingress.kubernetes.io/proxy-body-size: "0"
|
||||
|
||||
adminserver:
|
||||
image:
|
||||
repository: vmware/harbor-adminserver
|
||||
tag: v1.3.0
|
||||
tag: *harbor_image_tag
|
||||
pullPolicy: IfNotPresent
|
||||
emailHost: "smtp.mydomain.com"
|
||||
emailPort: "25"
|
||||
@ -110,7 +63,7 @@ adminserver:
|
||||
emailSsl: "false"
|
||||
emailFrom: "admin <sample_admin@mydomain.com>"
|
||||
emailIdentity: ""
|
||||
key: not-a-secure-key
|
||||
emailInsecure: "False"
|
||||
emailPwd: not-a-secure-password
|
||||
harborAdminPassword: Harbor12345
|
||||
## Persist data to a persistent volume
|
||||
@ -129,9 +82,8 @@ adminserver:
|
||||
jobservice:
|
||||
image:
|
||||
repository: vmware/harbor-jobservice
|
||||
tag: v1.3.0
|
||||
tag: *harbor_image_tag
|
||||
pullPolicy: IfNotPresent
|
||||
key: not-a-secure-key
|
||||
secret: not-a-secure-secret
|
||||
# resources:
|
||||
# requests:
|
||||
@ -143,10 +95,9 @@ jobservice:
|
||||
ui:
|
||||
image:
|
||||
repository: vmware/harbor-ui
|
||||
tag: v1.3.0
|
||||
tag: *harbor_image_tag
|
||||
pullPolicy: IfNotPresent
|
||||
secret: not-a-secure-secret
|
||||
key: not-a-secure-key
|
||||
privateKeyPem: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKAIBAAKCAgEA4WYbxdrFGG6RnfyYKlHYML3lEqtA9cYWWOynE9BeaEr/cMnM
|
||||
@ -211,7 +162,7 @@ ui:
|
||||
mysql:
|
||||
image:
|
||||
repository: vmware/harbor-db
|
||||
tag: v1.3.0
|
||||
tag: *harbor_image_tag
|
||||
pullPolicy: IfNotPresent
|
||||
# If left blank will use the included mysql service name.
|
||||
host: ~
|
||||
@ -231,8 +182,8 @@ mysql:
|
||||
|
||||
registry:
|
||||
image:
|
||||
repository: registry
|
||||
tag: "2.6.2"
|
||||
repository: vmware/registry-photon
|
||||
tag: v2.6.2-v1.4.0
|
||||
pullPolicy: IfNotPresent
|
||||
httpSecret: not-a-secure-secret
|
||||
logLevel:
|
||||
@ -296,12 +247,12 @@ registry:
|
||||
## Enabling it will just break things.
|
||||
#
|
||||
clair:
|
||||
enabled: false
|
||||
postgresPassword: not-a-secure-password
|
||||
enabled: true
|
||||
image:
|
||||
repository: vmware/clair
|
||||
tag: v2.0.1-photon
|
||||
repository: vmware/clair-photon
|
||||
tag: v2.0.1-v1.4.0
|
||||
pullPolicy: IfNotPresent
|
||||
postgresPassword: not-a-secure-password
|
||||
pgImage:
|
||||
repository: postgres
|
||||
tag: "9.6.4"
|
||||
|
@ -995,6 +995,86 @@ paths:
|
||||
description: Forbidden.
|
||||
'404':
|
||||
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}':
|
||||
get:
|
||||
summary: Get the tag of the repository.
|
||||
@ -1075,6 +1155,101 @@ paths:
|
||||
$ref: '#/definitions/DetailedTag'
|
||||
'500':
|
||||
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':
|
||||
get:
|
||||
summary: Get manifests of a relevant repository.
|
||||
@ -1639,6 +1814,164 @@ paths:
|
||||
project and target.
|
||||
'500':
|
||||
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:
|
||||
post:
|
||||
summary: Trigger the replication according to the specified policy.
|
||||
@ -2881,6 +3214,11 @@ definitions:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ComponentOverviewEntry'
|
||||
labels:
|
||||
type: array
|
||||
description: The label list.
|
||||
items:
|
||||
$ref: '#/definitions/Label'
|
||||
ComponentOverviewEntry:
|
||||
type: object
|
||||
properties:
|
||||
@ -2914,6 +3252,11 @@ definitions:
|
||||
tags_count:
|
||||
type: integer
|
||||
description: The tags count of repository.
|
||||
labels:
|
||||
type: array
|
||||
description: The label list.
|
||||
items:
|
||||
$ref: '#/definitions/Label'
|
||||
creation_time:
|
||||
type: string
|
||||
description: The creation time of repository.
|
||||
@ -3056,3 +3399,30 @@ definitions:
|
||||
status:
|
||||
type: string
|
||||
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.
|
||||
|
@ -12,6 +12,10 @@ LDAP_UID=$ldap_uid
|
||||
LDAP_SCOPE=$ldap_scope
|
||||
LDAP_TIMEOUT=$ldap_timeout
|
||||
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
|
||||
MYSQL_HOST=$db_host
|
||||
MYSQL_PORT=$db_port
|
||||
|
@ -91,6 +91,18 @@ ldap_timeout = 5
|
||||
#Verify certificate from LDAP server
|
||||
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
|
||||
self_registration = on
|
||||
|
||||
|
@ -254,6 +254,41 @@ create table properties (
|
||||
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` (
|
||||
`version_num` varchar(32) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
@ -111,7 +111,7 @@ create table project_metadata (
|
||||
creation_time timestamp,
|
||||
update_time timestamp,
|
||||
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)
|
||||
);
|
||||
|
||||
@ -240,6 +240,47 @@ create table properties (
|
||||
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 (
|
||||
version_num varchar(32) NOT NULL
|
||||
);
|
||||
|
@ -224,6 +224,10 @@ ldap_uid = rcp.get("configuration", "ldap_uid")
|
||||
ldap_scope = rcp.get("configuration", "ldap_scope")
|
||||
ldap_timeout = rcp.get("configuration", "ldap_timeout")
|
||||
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_host = rcp.get("configuration", "db_host")
|
||||
db_user = rcp.get("configuration", "db_user")
|
||||
@ -325,6 +329,10 @@ render(os.path.join(templates_dir, "adminserver", "env"),
|
||||
ldap_scope=ldap_scope,
|
||||
ldap_verify_cert=ldap_verify_cert,
|
||||
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_host=db_host,
|
||||
db_user=db_user,
|
||||
|
14
src/adminserver/api/monitor.go
Normal file
14
src/adminserver/api/monitor.go
Normal 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
|
||||
}
|
||||
}
|
16
src/adminserver/api/monitor_test.go
Normal file
16
src/adminserver/api/monitor_test.go
Normal 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))
|
||||
}
|
@ -31,7 +31,10 @@ func NewHandler() http.Handler {
|
||||
"uiSecret": os.Getenv("UI_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)
|
||||
return h
|
||||
}
|
||||
@ -39,12 +42,14 @@ func NewHandler() http.Handler {
|
||||
type authHandler struct {
|
||||
authenticator auth.Authenticator
|
||||
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{
|
||||
authenticator: authenticator,
|
||||
handler: handler,
|
||||
insecureAPIs: insecureAPIs,
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +61,12 @@ func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
if err != nil {
|
||||
log.Errorf("failed to authenticate request: %v", err)
|
||||
|
@ -45,28 +45,40 @@ func TestNewAuthHandler(t *testing.T) {
|
||||
cases := []struct {
|
||||
authenticator auth.Authenticator
|
||||
handler http.Handler
|
||||
insecureAPIs map[string]bool
|
||||
responseCode int
|
||||
requestURL string
|
||||
}{
|
||||
|
||||
{nil, nil, http.StatusOK},
|
||||
{nil, nil, nil, http.StatusOK,"http://localhost/good"},
|
||||
{&fakeAuthenticator{
|
||||
authenticated: false,
|
||||
err: nil,
|
||||
}, nil, http.StatusUnauthorized},
|
||||
}, nil, nil, http.StatusUnauthorized,"http://localhost/hello"},
|
||||
{&fakeAuthenticator{
|
||||
authenticated: false,
|
||||
err: errors.New("error"),
|
||||
}, nil, http.StatusInternalServerError},
|
||||
}, nil, nil, http.StatusInternalServerError,"http://localhost/hello"},
|
||||
{&fakeAuthenticator{
|
||||
authenticated: true,
|
||||
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 {
|
||||
handler := newAuthHandler(c.authenticator, c.handler)
|
||||
handler := newAuthHandler(c.authenticator, c.handler, c.insecureAPIs)
|
||||
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")
|
||||
}
|
||||
handler := NewHandler()
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET","http://localhost/api/ping",nil)
|
||||
handler.ServeHTTP(w,r)
|
||||
|
||||
}
|
||||
|
@ -27,5 +27,6 @@ func newRouter() http.Handler {
|
||||
r.HandleFunc("/api/configurations", api.UpdateCfgs).Methods("PUT")
|
||||
r.HandleFunc("/api/configurations/reset", api.ResetCfgs).Methods("POST")
|
||||
r.HandleFunc("/api/systeminfo/capacity", api.Capacity).Methods("GET")
|
||||
r.HandleFunc("/api/ping", api.Ping).Methods("GET")
|
||||
return r
|
||||
}
|
||||
|
@ -89,6 +89,13 @@ var (
|
||||
env: "LDAP_VERIFY_CERT",
|
||||
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.EmailPort: &parser{
|
||||
env: "EMAIL_PORT",
|
||||
@ -152,6 +159,13 @@ var (
|
||||
repeatLoadEnvs = map[string]interface{}{
|
||||
common.ExtEndpoint: "EXT_ENDPOINT",
|
||||
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{
|
||||
env: "MAX_JOB_WORKERS",
|
||||
parse: parseStringToInt,
|
||||
@ -170,6 +184,12 @@ var (
|
||||
parse: parseStringToBool,
|
||||
},
|
||||
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.UAAClientID: "UAA_CLIENTID",
|
||||
common.UAAClientSecret: "UAA_CLIENTSECRET",
|
||||
@ -382,4 +402,5 @@ func validLdapScope(cfg map[string]interface{}, isMigrate bool) {
|
||||
ldapScope = 0
|
||||
}
|
||||
cfg[ldapScopeKey] = ldapScope
|
||||
|
||||
}
|
||||
|
@ -29,6 +29,15 @@ const (
|
||||
RoleDeveloper = 2
|
||||
RoleGuest = 3
|
||||
|
||||
LabelLevelSystem = "s"
|
||||
LabelLevelUser = "u"
|
||||
LabelScopeGlobal = "g"
|
||||
LabelScopeProject = "p"
|
||||
|
||||
ResourceTypeProject = "p"
|
||||
ResourceTypeRepository = "r"
|
||||
ResourceTypeImage = "i"
|
||||
|
||||
ExtEndpoint = "ext_endpoint"
|
||||
AUTHMode = "auth_mode"
|
||||
DatabaseType = "database_type"
|
||||
@ -50,6 +59,10 @@ const (
|
||||
LDAPScope = "ldap_scope"
|
||||
LDAPTimeout = "ldap_timeout"
|
||||
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"
|
||||
RegistryURL = "registry_url"
|
||||
EmailHost = "email_host"
|
||||
|
99
src/common/dao/label.go
Normal file
99
src/common/dao/label.go
Normal 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
|
||||
}
|
91
src/common/dao/label_test.go
Normal file
91
src/common/dao/label_test.go
Normal 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)
|
||||
}
|
74
src/common/dao/resource_label.go
Normal file
74
src/common/dao/resource_label.go
Normal 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
|
||||
}
|
74
src/common/dao/resource_label_test.go
Normal file
74
src/common/dao/resource_label_test.go
Normal 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))
|
||||
}
|
6
src/common/job/const.go
Normal file
6
src/common/job/const.go
Normal file
@ -0,0 +1,6 @@
|
||||
package job
|
||||
|
||||
const (
|
||||
//ImageScanJob is name of scan job it will be used as key to register to job service.
|
||||
ImageScanJob = "IMAGE_SCAN"
|
||||
)
|
11
src/common/job/parms.go
Normal file
11
src/common/job/parms.go
Normal file
@ -0,0 +1,11 @@
|
||||
package job
|
||||
|
||||
type ScanJobParms struct {
|
||||
JobID int64 `json:"job_int_id"`
|
||||
Repository string `json:"repository"`
|
||||
Tag string `json:"tag"`
|
||||
Secret string `json: "job_service_secret"`
|
||||
RegistryURL string `json:"registry_url"`
|
||||
ClairEndpoint string `json:"clair_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
}
|
@ -32,5 +32,7 @@ func init() {
|
||||
new(ClairVulnTimestamp),
|
||||
new(WatchItem),
|
||||
new(ProjectMetadata),
|
||||
new(ConfigEntry))
|
||||
new(ConfigEntry),
|
||||
new(Label),
|
||||
new(ResourceLabel))
|
||||
}
|
||||
|
81
src/common/models/label.go
Normal file
81
src/common/models/label.go
Normal 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"
|
||||
}
|
86
src/common/models/label_test.go
Normal file
86
src/common/models/label_test.go
Normal 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())
|
||||
}
|
||||
}
|
@ -27,12 +27,21 @@ type LdapConf struct {
|
||||
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 ...
|
||||
type LdapUser struct {
|
||||
Username string `json:"ldap_username"`
|
||||
Email string `json:"ldap_email"`
|
||||
Realname string `json:"ldap_realname"`
|
||||
DN string `json:"-"`
|
||||
Username string `json:"ldap_username"`
|
||||
Email string `json:"ldap_email"`
|
||||
Realname string `json:"ldap_realname"`
|
||||
DN string `json:"-"`
|
||||
GroupDNList []string `json:"ldap_groupdn"`
|
||||
}
|
||||
|
||||
//LdapImportUser ...
|
||||
@ -45,3 +54,9 @@ type LdapFailedImportUser struct {
|
||||
UID string `json:"uid"`
|
||||
Error string `json:"err_msg"`
|
||||
}
|
||||
|
||||
// LdapGroup ...
|
||||
type LdapGroup struct {
|
||||
GroupName string `json:"group_name,omitempty"`
|
||||
GroupDN string `json:"group_dn,omitempty"`
|
||||
}
|
||||
|
@ -95,3 +95,8 @@ func transformVuln(clairVuln *models.ClairLayerEnvelope) (*models.ComponentsOver
|
||||
Summary: compSummary,
|
||||
}, 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)
|
||||
}
|
||||
|
@ -130,6 +130,7 @@ func newClient(addr, identity, username, password string,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tls = true
|
||||
} else {
|
||||
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 {
|
||||
log.Debug("authenticating the client...")
|
||||
// only support plain auth
|
||||
if err = client.Auth(smtp.PlainAuth(identity,
|
||||
username, password, host)); err != nil {
|
||||
var auth smtp.Auth
|
||||
if tls {
|
||||
auth = smtp.PlainAuth(identity, username, password, host)
|
||||
} else {
|
||||
auth = smtp.CRAMMD5Auth(username, password)
|
||||
}
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
|
@ -185,6 +185,7 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) {
|
||||
|
||||
for _, ldapEntry := range result.Entries {
|
||||
var u models.LdapUser
|
||||
groupDNList := []string{}
|
||||
for _, attr := range ldapEntry.Attributes {
|
||||
//OpenLdap sometimes contain leading space in useranme
|
||||
val := strings.TrimSpace(attr.Values[0])
|
||||
@ -200,7 +201,10 @@ func (session *Session) SearchUser(username string) ([]models.LdapUser, error) {
|
||||
u.Email = val
|
||||
case "email":
|
||||
u.Email = val
|
||||
case "memberof":
|
||||
groupDNList = append(groupDNList, val)
|
||||
}
|
||||
u.GroupDNList = groupDNList
|
||||
}
|
||||
u.DN = ldapEntry.DN
|
||||
ldapUsers = append(ldapUsers, u)
|
||||
@ -248,20 +252,28 @@ func (session *Session) Open() error {
|
||||
|
||||
// SearchLdap to search ldap with the provide filter
|
||||
func (session *Session) SearchLdap(filter 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)
|
||||
}
|
||||
|
||||
attributes := []string{"uid", "cn", "mail", "email"}
|
||||
attributes := []string{"uid", "cn", "mail", "email", "memberof"}
|
||||
lowerUID := strings.ToLower(session.ldapConfig.LdapUID)
|
||||
|
||||
if lowerUID != "uid" && lowerUID != "cn" && lowerUID != "mail" && lowerUID != "email" {
|
||||
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)
|
||||
searchRequest := goldap.NewSearchRequest(
|
||||
session.ldapConfig.LdapBaseDn,
|
||||
baseDN,
|
||||
session.ldapConfig.LdapScope,
|
||||
goldap.NeverDerefAliases,
|
||||
0, //Unlimited results
|
||||
@ -318,3 +330,69 @@ func (session *Session) 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
|
||||
}
|
||||
|
@ -15,15 +15,16 @@ package ldap
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
|
||||
"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/log"
|
||||
"github.com/vmware/harbor/src/common/utils/test"
|
||||
uiConfig "github.com/vmware/harbor/src/ui/config"
|
||||
goldap "gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
var adminServerLdapTestConfig = map[string]interface{}{
|
||||
@ -217,6 +218,14 @@ func TestSearchUser(t *testing.T) {
|
||||
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) {
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,10 @@ var adminServerDefaultConfig = map[string]interface{}{
|
||||
common.LDAPFilter: "",
|
||||
common.LDAPScope: 3,
|
||||
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.RegistryURL: "http://registry",
|
||||
common.EmailHost: "127.0.0.1",
|
||||
|
139
src/jobservice_v2/job/impl/scan/job.go
Normal file
139
src/jobservice_v2/job/impl/scan/job.go
Normal 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
|
||||
}
|
85
src/jobservice_v2/job/impl/utils/utils.go
Normal file
85
src/jobservice_v2/job/impl/utils/utils.go
Normal 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
|
||||
}
|
@ -10,12 +10,14 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/vmware/harbor/src/common/job"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
"github.com/vmware/harbor/src/jobservice_v2/api"
|
||||
"github.com/vmware/harbor/src/jobservice_v2/config"
|
||||
"github.com/vmware/harbor/src/jobservice_v2/core"
|
||||
"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/scan"
|
||||
"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
|
||||
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()
|
||||
|
||||
|
@ -41,6 +41,10 @@ var (
|
||||
common.LDAPScope,
|
||||
common.LDAPTimeout,
|
||||
common.LDAPVerifyCert,
|
||||
common.LDAPGroupAttributeName,
|
||||
common.LDAPGroupBaseDN,
|
||||
common.LDAPGroupSearchFilter,
|
||||
common.LDAPGroupSearchScope,
|
||||
common.EmailHost,
|
||||
common.EmailPort,
|
||||
common.EmailUsername,
|
||||
@ -66,6 +70,9 @@ var (
|
||||
common.LDAPBaseDN,
|
||||
common.LDAPUID,
|
||||
common.LDAPFilter,
|
||||
common.LDAPGroupAttributeName,
|
||||
common.LDAPGroupBaseDN,
|
||||
common.LDAPGroupSearchFilter,
|
||||
common.EmailHost,
|
||||
common.EmailUsername,
|
||||
common.EmailPassword,
|
||||
@ -80,6 +87,7 @@ var (
|
||||
common.EmailPort,
|
||||
common.LDAPScope,
|
||||
common.LDAPTimeout,
|
||||
common.LDAPGroupSearchScope,
|
||||
common.TokenExpiration,
|
||||
}
|
||||
|
||||
|
@ -111,6 +111,10 @@ func init() {
|
||||
beego.Router("/api/users/?:id", &UserAPI{})
|
||||
beego.Router("/api/logs", &LogAPI{})
|
||||
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", &RepositoryAPI{}, "get:GetTags")
|
||||
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/email/ping", &EmailAPI{}, "post:Ping")
|
||||
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")
|
||||
|
||||
if err := core.Init(); err != nil {
|
||||
@ -985,6 +991,11 @@ func (a testapi) GetGeneralInfo() (int, []byte, error) {
|
||||
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
|
||||
func (a testapi) CertGet(authInfo usrInfo) (int, []byte, error) {
|
||||
_sling := sling.New().Get(a.basePath)
|
||||
|
263
src/ui/api/label.go
Normal file
263
src/ui/api/label.go
Normal 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
435
src/ui/api/label_test.go
Normal 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...)
|
||||
}
|
@ -42,7 +42,7 @@ type ProjectAPI struct {
|
||||
project *models.Project
|
||||
}
|
||||
|
||||
const projectNameMaxLen int = 30
|
||||
const projectNameMaxLen int = 255
|
||||
const projectNameMinLen int = 2
|
||||
const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*`
|
||||
|
||||
@ -491,7 +491,7 @@ func (p *ProjectAPI) Logs() {
|
||||
func validateProjectReq(req *models.ProjectRequest) error {
|
||||
pn := req.Name
|
||||
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 + `$`)
|
||||
legal := validProjectName.MatchString(pn)
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"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/models"
|
||||
"github.com/vmware/harbor/src/common/notifier"
|
||||
@ -47,15 +48,16 @@ type RepositoryAPI struct {
|
||||
}
|
||||
|
||||
type repoResp struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProjectID int64 `json:"project_id"`
|
||||
Description string `json:"description"`
|
||||
PullCount int64 `json:"pull_count"`
|
||||
StarCount int64 `json:"star_count"`
|
||||
TagsCount int64 `json:"tags_count"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProjectID int64 `json:"project_id"`
|
||||
Description string `json:"description"`
|
||||
PullCount int64 `json:"pull_count"`
|
||||
StarCount int64 `json:"star_count"`
|
||||
TagsCount int64 `json:"tags_count"`
|
||||
Labels []*models.Label `json:"labels"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
UpdateTime time.Time `json:"update_time"`
|
||||
}
|
||||
|
||||
type tagDetail struct {
|
||||
@ -78,6 +80,7 @@ type tagResp struct {
|
||||
tagDetail
|
||||
Signature *notary.Target `json:"signature"`
|
||||
ScanOverview *models.ImgScanOverview `json:"scan_overview,omitempty"`
|
||||
Labels []*models.Label `json:"labels"`
|
||||
}
|
||||
|
||||
type manifestResp struct {
|
||||
@ -145,10 +148,10 @@ func getRepositories(projectID int64, keyword string,
|
||||
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{}
|
||||
for _, repository := range repositories {
|
||||
repo := &repoResp{
|
||||
@ -167,6 +170,15 @@ func populateTagsCount(repositories []*models.RepoRecord) ([]*repoResp, error) {
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
}
|
||||
return result, nil
|
||||
@ -252,6 +264,11 @@ func (ra *RepositoryAPI) Delete() {
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
image := fmt.Sprintf("%s:%s", repoName, t)
|
||||
if err = dao.DeleteLabelsOfResource(common.ResourceTypeImage, image); err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to delete labels of image %s: %v", image, err))
|
||||
return
|
||||
}
|
||||
if err = rc.DeleteTag(t); err != nil {
|
||||
if regErr, ok := err.(*registry_error.HTTPError); ok {
|
||||
if regErr.StatusCode == http.StatusNotFound {
|
||||
@ -296,6 +313,22 @@ func (ra *RepositoryAPI) Delete() {
|
||||
ra.CustomAbort(http.StatusInternalServerError, "")
|
||||
}
|
||||
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 {
|
||||
log.Errorf("failed to delete repository %s: %v", repoName, err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "")
|
||||
@ -343,7 +376,7 @@ func (ra *RepositoryAPI) GetTag() {
|
||||
return
|
||||
}
|
||||
|
||||
result := assemble(client, repository, []string{tag},
|
||||
result := assembleTags(client, repository, []string{tag},
|
||||
ra.SecurityCtx.GetUsername())
|
||||
ra.Data["json"] = result[0]
|
||||
ra.ServeJSON()
|
||||
@ -387,13 +420,13 @@ func (ra *RepositoryAPI) GetTags() {
|
||||
return
|
||||
}
|
||||
|
||||
ra.Data["json"] = assemble(client, repoName, tags, ra.SecurityCtx.GetUsername())
|
||||
ra.Data["json"] = assembleTags(client, repoName, tags, ra.SecurityCtx.GetUsername())
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
// get config, signature and scan overview and assemble them into one
|
||||
// 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 {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -633,7 +675,7 @@ func (ra *RepositoryAPI) GetTopRepos() {
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal server error")
|
||||
}
|
||||
|
||||
result, err := populateTagsCount(repos)
|
||||
result, err := assembleRepos(repos)
|
||||
if err != nil {
|
||||
log.Errorf("failed to popultate tags count to repositories: %v", err)
|
||||
ra.CustomAbort(http.StatusInternalServerError, "internal server error")
|
||||
|
248
src/ui/api/repository_label.go
Normal file
248
src/ui/api/repository_label.go
Normal 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
|
||||
}
|
253
src/ui/api/repository_label_test.go
Normal file
253
src/ui/api/repository_label_test.go
Normal 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))
|
||||
}
|
@ -247,3 +247,9 @@ func getClairVulnStatus() *models.ClairVulnerabilityStatus {
|
||||
res.Details = details
|
||||
return res
|
||||
}
|
||||
|
||||
// Ping ping the harbor UI service.
|
||||
func (sia *SystemInfoAPI) Ping() {
|
||||
sia.Data["json"] = "Pong"
|
||||
sia.ServeJSON()
|
||||
}
|
||||
|
@ -91,3 +91,10 @@ func TestGetCert(t *testing.T) {
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/vmware/harbor/src/adminserver/client"
|
||||
@ -205,6 +206,35 @@ func LDAPConf() (*models.LdapConf, error) {
|
||||
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)
|
||||
func TokenExpiration() (int, error) {
|
||||
cfg, err := mg.Get()
|
||||
|
@ -75,6 +75,10 @@ func TestConfig(t *testing.T) {
|
||||
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 {
|
||||
t.Fatalf("failed to get token expiration: %v", err)
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ func initRouters() {
|
||||
}
|
||||
|
||||
// API
|
||||
beego.Router("/api/ping", &api.SystemInfoAPI{}, "get:Ping")
|
||||
beego.Router("/api/search", &api.SearchAPI{})
|
||||
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post")
|
||||
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/scanAll", &api.RepositoryAPI{}, "post:ScanAll")
|
||||
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/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/:tag/scan", &api.RepositoryAPI{}, "post:ScanImage")
|
||||
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/statistics", &api.StatisticAPI{})
|
||||
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/volumes", &api.SystemInfoAPI{}, "get:GetVolumeInfo")
|
||||
@ -108,6 +115,7 @@ func initRouters() {
|
||||
beego.Router("/service/token", &token.Handler{})
|
||||
|
||||
beego.Router("/registryproxy/*", &controllers.RegistryProxy{}, "*:Handle")
|
||||
|
||||
//Error pages
|
||||
beego.ErrorController(&controllers.ErrorController{})
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
**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**
|
||||
|
@ -1,5 +1,70 @@
|
||||
export const CREATE_EDIT_RULE_STYLE: string = `
|
||||
.form-group-label-override {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}`;
|
||||
/**
|
||||
* Created by pengf on 9/28/2017.
|
||||
*/
|
||||
|
||||
.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;}
|
||||
`;
|
@ -1,100 +1,134 @@
|
||||
export const CREATE_EDIT_RULE_TEMPLATE: string = `
|
||||
<clr-modal [(clrModalOpen)]="createEditRuleOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<h3 class="modal-title">{{modalTitle}}</h3>
|
||||
<clr-modal [(clrModalOpen)]="createEditRuleOpened" [clrModalStaticBackdrop]="true" [clrModalClosable]="false">
|
||||
<h3 class="modal-title">{{headerTitle | translate}}</h3>
|
||||
<hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
|
||||
<div class="modal-body" style="max-height: 85vh;">
|
||||
<form #ruleForm="ngForm">
|
||||
<section class="form-block">
|
||||
<div class="alert alert-warning" *ngIf="!editable">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="alert-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
{{'REPLICATION.CANNOT_EDIT' | translate}}
|
||||
</span>
|
||||
</div>
|
||||
<form [formGroup]="ruleForm" novalidate>
|
||||
<section class="form-block">
|
||||
<div class="form-group form-group-override">
|
||||
<label class="form-group-label-override required">{{'REPLICATION.NAME' | translate}}</label>
|
||||
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
|
||||
[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>
|
||||
<!--Description-->
|
||||
<div class="form-group form-group-override">
|
||||
<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}} {{'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 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>
|
||||
</div>
|
||||
<label *ngIf="noProjectInfo.length != 0" class="colorRed alertLabel">{{noProjectInfo | translate}}</label>
|
||||
</div>
|
||||
|
||||
<!--images/Filter-->
|
||||
<div class="form-group form-group-override">
|
||||
<label class="form-group-label-override">{{'REPLICATION.SOURCE_IMAGES_FILTER' | translate}}</label>
|
||||
<div formArrayName="filters">
|
||||
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index" [formGroupName]="i">
|
||||
<div>
|
||||
<div class="select floatSetPar">
|
||||
<select formControlName="kind" (change)="filterChange($event)" id="{{i}}" name="{{filterListData[i]?.name}}">
|
||||
<option *ngFor="let filter of filterListData[i]?.options;" value="{{filter}}">{{filter}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
|
||||
[class.invalid]='ruleForm.controls.filters.controls[i].controls.pattern.touched && ruleForm.controls.filters.controls[i].controls.pattern.invalid'>
|
||||
<input type="text" #filterValue required size="14" formControlName="pattern">
|
||||
<span class="tooltip-content">{{'TOOLTIP.EMPTY' | translate}}</span>
|
||||
</label>
|
||||
<clr-icon shape="times-circle" class="is-solid" (click)="deleteFilter(i)"></clr-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<clr-icon shape="plus-circle" class="is-solid" [hidden]="isFilterHide" (click)="addNewFilter()" style="margin-top: 11px;"></clr-icon>
|
||||
</div>
|
||||
<!--Targets-->
|
||||
<div class="form-group form-group-override">
|
||||
<label class="form-group-label-override required">{{'DESTINATION.ENDPOINT' | translate}}</label>
|
||||
<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>
|
||||
<label *ngIf="noEndpointInfo.length != 0" class="colorRed alertLabel">{{noEndpointInfo | translate}}</label>
|
||||
<span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'SIDE_NAV.SYSTEM_MGMT.REGISTRY' | translate}}</span>
|
||||
</div>
|
||||
|
||||
<!--Trigger-->
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 form-group-label-override">{{'REPLICATION.ENABLE' | translate}}</label>
|
||||
<div class="checkbox-inline">
|
||||
<input type="checkbox" id="policy_enable" [(ngModel)]="createEditRule.enable" name="enable" #enable="ngModel" [disabled]="untoggleable">
|
||||
<label for="policy_enable"></label>
|
||||
<!--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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="destination_name" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_NAME' | translate}}<span style="color: red">*</span></label>
|
||||
<div class="select" *ngIf="!isCreateEndpoint">
|
||||
<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>
|
||||
</div>
|
||||
<label class="col-md-8" *ngIf="isCreateEndpoint" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="endpointName.errors && (endpointName.dirty || endpointName.touched)"
|
||||
class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
|
||||
<input type="text" id="destination_name" [(ngModel)]="createEditRule.endpointName" name="endpointName" size="8" #endpointName="ngModel" value="" required>
|
||||
<span class="tooltip-content" *ngIf="endpointName.errors && endpointName.errors.required && (endpointName.dirty || endpointName.touched)">
|
||||
{{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}}
|
||||
</span>
|
||||
</label>
|
||||
<div class="checkbox-inline" *ngIf="showNewDestination">
|
||||
<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>
|
||||
<!--weekly-->
|
||||
<span [hidden]="!weeklySchedule || !isScheduleOpt">on </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 </span>
|
||||
<input [hidden]="!isScheduleOpt" type="time" formControlName="offtime" required value="08:00" />
|
||||
</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 class="form-group">
|
||||
<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 class="form-group">
|
||||
<label for="destination_password" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label>
|
||||
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="readonly || !isCreateEndpoint"
|
||||
[(ngModel)]="createEditRule.password" size="20" name="password" #password="ngModel">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="destination_insecure" class="col-md-4 form-group-label-override">{{'CONFIG.VERIFY_REMOTE_CERT' | translate }}</label>
|
||||
<clr-checkbox #insecure class="col-md-8" name="insecure" id="destination_insecure" [clrChecked]="!createEditRule.insecure" [clrDisabled]="readonly || !isCreateEndpoint || testOngoing" (clrCheckedChange)="setInsecureValue($event)">
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right" style="top:-7px;">
|
||||
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{'CONFIG.TOOLTIP.VERIFY_REMOTE_CERT' | translate}}</span>
|
||||
</a>
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="spin" class="col-md-4"></label>
|
||||
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
|
||||
<span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div style="width: 100%;" [hidden]="!isImmediate">
|
||||
<clr-checkbox [clrChecked]="false" id="ruleDeletion" formControlName="replicate_deletion">
|
||||
{{'REPLICATION.DELETE_REMOTE_IMAGES' | translate}}
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
<div style="width: 100%;" >
|
||||
<clr-checkbox [clrChecked]="true" id="ruleExit" formControlName="replicate_existing_image_now">
|
||||
{{'REPLICATION.REPLICATE_IMMEDIATE' | translate}}
|
||||
</clr-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:block;text-align:center">
|
||||
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<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" class="btn btn-outline" [disabled]="btnAbled" (click)="onCancel()">{{'BUTTON.CANCEL' | translate }}</button>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="!ruleForm.form.valid || testOngoing || !editable" (click)="onSubmit()">{{'BUTTON.OK' | translate}}</button>
|
||||
<button type="button" id="ruleBtnCancel" class="btn btn-outline" [disabled]="this.inProgress" (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>
|
||||
</div>
|
||||
</clr-modal>`;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
@ -25,52 +25,55 @@ import {
|
||||
JobLogDefaultService
|
||||
} from '../service/index';
|
||||
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 {Project} from "../project-policy-config/project";
|
||||
|
||||
describe('CreateEditRuleComponent (inline template)', ()=>{
|
||||
|
||||
let mockRules: 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,
|
||||
"deleted": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"project_id": 1,
|
||||
"project_name": "library",
|
||||
"target_id": 3,
|
||||
"target_name": "target_02",
|
||||
"name": "sync_02",
|
||||
"enabled": 1,
|
||||
"description": "",
|
||||
"cron_str": "",
|
||||
"error_job_count": 1,
|
||||
"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
|
||||
}
|
||||
];
|
||||
|
||||
"projects": [{ "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": '',
|
||||
}
|
||||
}],
|
||||
"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[] = [
|
||||
{
|
||||
"id": 1,
|
||||
@ -144,17 +147,68 @@ describe('CreateEditRuleComponent (inline template)', ()=>{
|
||||
|
||||
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,
|
||||
"deleted": 0
|
||||
};
|
||||
"projects": [{ "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": '',
|
||||
}
|
||||
}],
|
||||
"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 fixtureCreate: ComponentFixture<CreateEditRuleComponent>;
|
||||
@ -164,7 +218,7 @@ describe('CreateEditRuleComponent (inline template)', ()=>{
|
||||
|
||||
let replicationService: ReplicationService;
|
||||
let endpointService: EndpointService;
|
||||
|
||||
|
||||
let spyRules: jasmine.Spy;
|
||||
let spyOneRule: jasmine.Spy;
|
||||
|
||||
@ -172,12 +226,11 @@ describe('CreateEditRuleComponent (inline template)', ()=>{
|
||||
let spyEndpoint: jasmine.Spy;
|
||||
|
||||
let config: IServiceConfig = {
|
||||
replicationRuleEndpoint: '/api/policies/replication/testing',
|
||||
replicationJobEndpoint: '/api/jobs/replication/testing',
|
||||
targetBaseEndpoint: '/api/targets/testing'
|
||||
};
|
||||
|
||||
beforeEach(async(()=>{
|
||||
beforeEach(async(() =>{
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
@ -198,6 +251,7 @@ describe('CreateEditRuleComponent (inline template)', ()=>{
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: ReplicationService, useClass: ReplicationDefaultService },
|
||||
{ provide: EndpointService, useClass: EndpointDefaultService },
|
||||
{ provide: ProjectService, useClass: ProjectDefaultService },
|
||||
{ provide: JobLogService, useClass: JobLogDefaultService }
|
||||
]
|
||||
});
|
||||
@ -205,28 +259,27 @@ describe('CreateEditRuleComponent (inline template)', ()=>{
|
||||
|
||||
beforeEach(()=>{
|
||||
fixture = TestBed.createComponent(ReplicationComponent);
|
||||
|
||||
fixtureCreate = TestBed.createComponent(CreateEditRuleComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compCreate = fixtureCreate.componentInstance;
|
||||
comp.projectId = 1;
|
||||
comp.search.ruleId = 1;
|
||||
|
||||
replicationService = fixture.debugElement.injector.get(ReplicationService);
|
||||
|
||||
|
||||
|
||||
endpointService = fixtureCreate.debugElement.injector.get(EndpointService) ;
|
||||
|
||||
spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(Promise.resolve(mockRules));
|
||||
spyOneRule = spyOn(replicationService, 'getReplicationRule').and.returnValue(Promise.resolve(mockRule));
|
||||
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));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
|
||||
});
|
||||
|
||||
it('Should open creation modal and load endpoints', async(()=>{
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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 { DATETIME_PICKER_TEMPLATE } from './datetime-picker.component.html';
|
||||
@ -7,7 +7,7 @@ import { DATETIME_PICKER_TEMPLATE } from './datetime-picker.component.html';
|
||||
selector: 'hbr-datetime',
|
||||
template: DATETIME_PICKER_TEMPLATE
|
||||
})
|
||||
export class DatePickerComponent {
|
||||
export class DatePickerComponent implements OnChanges{
|
||||
|
||||
@Input() dateInput: string;
|
||||
@Input() oneDayOffset: boolean;
|
||||
@ -17,6 +17,10 @@ export class DatePickerComponent {
|
||||
|
||||
@Output() search = new EventEmitter<string>();
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.dateInput = this.dateInput.trim();
|
||||
}
|
||||
|
||||
get dateInvalid(): boolean {
|
||||
return (this.searchTime.errors && this.searchTime.errors.dateValidator && (this.searchTime.dirty || this.searchTime.touched)) || false;
|
||||
}
|
||||
|
@ -137,23 +137,13 @@ export class EndpointComponent implements OnInit, OnDestroy {
|
||||
|
||||
editTargets(targets: Endpoint[]) {
|
||||
if (targets && targets.length === 1) {
|
||||
let target= targets[0];
|
||||
let target = targets[0];
|
||||
let editable = true;
|
||||
if (!target.id) {
|
||||
return;
|
||||
}
|
||||
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.forceRefreshView(1000);
|
||||
})
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
this.createEditEndpointComponent.openCreateEditTarget(editable, id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ export * from './filter/index';
|
||||
export * from './endpoint/index';
|
||||
export * from './repository/index';
|
||||
export * from './create-edit-endpoint/index';
|
||||
export * from './create-edit-rule/index';
|
||||
export * from './repository-stackview/index';
|
||||
export * from './tag/index';
|
||||
export * from './list-replication-rule/index';
|
||||
|
@ -1,11 +1,11 @@
|
||||
export const LIST_REPLICATION_RULE_TEMPLATE: string = `
|
||||
<div style="padding-bottom: 15px;">
|
||||
<clr-datagrid [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedRow" [clDgRowSelection]="true">
|
||||
<clr-dg-action-bar style="height:24px;" *ngIf="opereateAvailable || isSystemAdmin">
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon> {{'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> {{'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> {{'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> {{'REPLICATION.REPLICATE' | translate}}</button>
|
||||
<clr-dg-action-bar style="height:24px;">
|
||||
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon> {{'REPLICATION.NEW_REPLICATION_RULE' | 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> {{'REPLICATION.EDIT_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> {{'REPLICATION.DELETE_POLICY' | 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> {{'REPLICATION.REPLICATE' | translate}}</button>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | 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-footer>
|
||||
</clr-datagrid>
|
||||
<confirmation-dialog #toggleConfirmDialog [batchInfors]="batchDelectionInfos" (confirmAction)="toggleConfirm($event)"></confirmation-dialog>
|
||||
<confirmation-dialog #deletionConfirmDialog [batchInfors]="batchDelectionInfos" (confirmAction)="deletionConfirm($event)"></confirmation-dialog>
|
||||
</div>
|
||||
`;
|
@ -14,66 +14,91 @@ import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { ReplicationService, ReplicationDefaultService } from '../service/replication.service';
|
||||
|
||||
|
||||
describe('ListReplicationRuleComponent (inline template)', ()=>{
|
||||
|
||||
let mockRules: ReplicationRule[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"project_id": 1,
|
||||
"project_name": "library",
|
||||
"target_id": 1,
|
||||
"target_name": "target_01",
|
||||
"projects": [{
|
||||
"project_id": 33,
|
||||
"owner_id": 1,
|
||||
"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",
|
||||
"enabled": 0,
|
||||
"description": "",
|
||||
"cron_str": "",
|
||||
"filters": null,
|
||||
"trigger": {"kind": "Manual", "schedule_param": null},
|
||||
"error_job_count": 2,
|
||||
"deleted": 0
|
||||
"replicate_deletion": false,
|
||||
"replicate_existing_image_now": false,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"project_id": 1,
|
||||
"project_name": "library",
|
||||
"target_id": 3,
|
||||
"target_name": "target_02",
|
||||
"name": "sync_02",
|
||||
"enabled": 1,
|
||||
"description": "",
|
||||
"cron_str": "",
|
||||
"error_job_count": 1,
|
||||
"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
|
||||
}
|
||||
"id": 2,
|
||||
"projects": [{
|
||||
"project_id": 33,
|
||||
"owner_id": 1,
|
||||
"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",
|
||||
"description": "",
|
||||
"filters": null,
|
||||
"trigger": {"kind": "Manual", "schedule_param": null},
|
||||
"error_job_count": 2,
|
||||
"replicate_deletion": false,
|
||||
"replicate_existing_image_now": false,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
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,
|
||||
"deleted": 0
|
||||
};
|
||||
|
||||
let fixture: ComponentFixture<ListReplicationRuleComponent>;
|
||||
|
||||
let comp: ListReplicationRuleComponent;
|
||||
|
@ -60,9 +60,8 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
||||
@Input() isSystemAdmin: boolean;
|
||||
@Input() selectedId: number | string;
|
||||
@Input() withReplicationJob: boolean;
|
||||
@Input() readonly: boolean;
|
||||
|
||||
@Input() loading: boolean = false;
|
||||
@Input() loading = false;
|
||||
|
||||
@Output() reload = new EventEmitter<boolean>();
|
||||
@Output() selectOne = new EventEmitter<ReplicationRule>();
|
||||
@ -100,10 +99,6 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
||||
setInterval(() => ref.markForCheck(), 500);
|
||||
}
|
||||
|
||||
public get opereateAvailable(): boolean {
|
||||
return !this.readonly && !this.projectId ? true : false;
|
||||
}
|
||||
|
||||
trancatedDescription(desc: string): string {
|
||||
if (desc.length > 35 ) {
|
||||
return desc.substr(0, 35);
|
||||
@ -135,7 +130,7 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
||||
|
||||
retrieveRules(ruleName: string = ''): void {
|
||||
this.loading = true;
|
||||
this.selectedRow = null;
|
||||
/*this.selectedRow = null;*/
|
||||
toPromise<ReplicationRule[]>(this.replicationService
|
||||
.getReplicationRules(this.projectId, ruleName))
|
||||
.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 {
|
||||
this.replicateManual.emit(rules);
|
||||
}
|
||||
@ -216,17 +181,6 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
|
||||
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> {
|
||||
let ruleData: ReplicationJobItem[];
|
||||
this.canDeleteRule = true;
|
||||
|
@ -1,18 +1,18 @@
|
||||
export class Project {
|
||||
project_id: number;
|
||||
owner_id: number;
|
||||
owner_id?: number;
|
||||
name: string;
|
||||
creation_time: Date | string;
|
||||
deleted: number;
|
||||
owner_name: string;
|
||||
togglable: boolean;
|
||||
update_time: Date | string;
|
||||
current_user_role_id: number;
|
||||
repo_count: number;
|
||||
has_project_admin_role: boolean;
|
||||
is_member: boolean;
|
||||
role_name: string;
|
||||
metadata: {
|
||||
creation_time?: Date | string;
|
||||
deleted?: number;
|
||||
owner_name?: string;
|
||||
togglable?: boolean;
|
||||
update_time?: Date | string;
|
||||
current_user_role_id?: number;
|
||||
repo_count?: number;
|
||||
has_project_admin_role?: boolean;
|
||||
is_member?: boolean;
|
||||
role_name?: string;
|
||||
metadata?: {
|
||||
public: string | boolean;
|
||||
enable_content_trust: string | boolean;
|
||||
prevent_vul: string | boolean;
|
||||
|
@ -11,7 +11,7 @@ export const REPLICATION_TEMPLATE: string = `
|
||||
</div>
|
||||
</div>
|
||||
<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 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">
|
||||
@ -73,5 +73,6 @@ export const REPLICATION_TEMPLATE: string = `
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>`;
|
@ -12,7 +12,7 @@ import { DatePickerComponent } from '../datetime-picker/datetime-picker.componen
|
||||
import { DateValidatorDirective } from '../datetime-picker/date-validator.directive';
|
||||
import { FilterComponent } from '../filter/filter.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 { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
@ -20,49 +20,92 @@ import { ReplicationService, ReplicationDefaultService } from '../service/replic
|
||||
import { EndpointService, EndpointDefaultService } from '../service/endpoint.service';
|
||||
import { JobLogViewerComponent } from '../job-log-viewer/job-log-viewer.component';
|
||||
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[] = [
|
||||
{
|
||||
"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,
|
||||
"deleted": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"project_id": 1,
|
||||
"project_name": "library",
|
||||
"target_id": 3,
|
||||
"target_name": "target_02",
|
||||
"name": "sync_02",
|
||||
"enabled": 1,
|
||||
"description": "",
|
||||
"cron_str": "",
|
||||
"error_job_count": 1,
|
||||
"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
|
||||
}
|
||||
{
|
||||
"id": 1,
|
||||
"projects": [{
|
||||
"project_id": 33,
|
||||
"owner_id": 1,
|
||||
"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",
|
||||
"description": "",
|
||||
"filters": null,
|
||||
"trigger": {"kind": "Manual", "schedule_param": null},
|
||||
"error_job_count": 2,
|
||||
"replicate_deletion": false,
|
||||
"replicate_existing_image_now": false,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"projects": [{
|
||||
"project_id": 33,
|
||||
"owner_id": 1,
|
||||
"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",
|
||||
"description": "",
|
||||
"filters": null,
|
||||
"trigger": {"kind": "Manual", "schedule_param": null},
|
||||
"error_job_count": 2,
|
||||
"replicate_deletion": false,
|
||||
"replicate_existing_image_now": false,
|
||||
}
|
||||
];
|
||||
|
||||
let mockJobs: ReplicationJobItem[] = [
|
||||
@ -81,7 +124,7 @@ describe('Replication Component (inline template)', ()=>{
|
||||
"repository": "library/mysql",
|
||||
"policy_id": 1,
|
||||
"operation": "transfer",
|
||||
"update_time": new Date("2017-05-27 12:20:33"),
|
||||
"update_time": new Date("2017-05-27 12:20:33"),
|
||||
"tags": null
|
||||
},
|
||||
{
|
||||
@ -95,71 +138,66 @@ describe('Replication Component (inline template)', ()=>{
|
||||
}
|
||||
];
|
||||
|
||||
let mockEndpoints: Endpoint[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"endpoint": "https://10.117.4.151",
|
||||
"name": "target_01",
|
||||
"username": "admin",
|
||||
"password": "",
|
||||
"insecure": false,
|
||||
"type": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"endpoint": "https://10.117.5.142",
|
||||
"name": "target_02",
|
||||
"username": "AAA",
|
||||
"password": "",
|
||||
"insecure": false,
|
||||
"type": 0
|
||||
},
|
||||
];
|
||||
|
||||
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 mockJob: ReplicationJob = {
|
||||
metadata: {xTotalCount: 3},
|
||||
data: mockJobs
|
||||
};
|
||||
|
||||
let mockEndpoints: Endpoint[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"endpoint": "https://10.117.4.151",
|
||||
"name": "target_01",
|
||||
"username": "admin",
|
||||
"password": "",
|
||||
"insecure": false,
|
||||
"type": 0
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"endpoint": "https://10.117.5.142",
|
||||
"name": "target_02",
|
||||
"username": "AAA",
|
||||
"password": "",
|
||||
"insecure": false,
|
||||
"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 = {
|
||||
"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,
|
||||
"deleted": 0
|
||||
};
|
||||
|
||||
let fixture: ComponentFixture<ReplicationComponent>;
|
||||
let fixtureCreate: ComponentFixture<CreateEditRuleComponent>;
|
||||
let comp: ReplicationComponent;
|
||||
let compCreate: CreateEditRuleComponent;
|
||||
|
||||
let replicationService: ReplicationService;
|
||||
let endpointService: EndpointService;
|
||||
|
||||
let spyRules: jasmine.Spy;
|
||||
let spyJobs: jasmine.Spy;
|
||||
let spyEndpoint: jasmine.Spy;
|
||||
|
||||
let deGrids: DebugElement[];
|
||||
let deRules: DebugElement;
|
||||
@ -194,23 +232,29 @@ describe('Replication Component (inline template)', ()=>{
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: ReplicationService, useClass: ReplicationDefaultService },
|
||||
{ provide: EndpointService, useClass: EndpointDefaultService },
|
||||
{ provide: ProjectService, useClass: ProjectDefaultService },
|
||||
{ provide: JobLogService, useClass: JobLogDefaultService }
|
||||
]
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(()=>{
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ReplicationComponent);
|
||||
|
||||
fixtureCreate = TestBed.createComponent(CreateEditRuleComponent);
|
||||
comp = fixture.componentInstance;
|
||||
compCreate = fixtureCreate.componentInstance;
|
||||
comp.projectId = 1;
|
||||
comp.search.ruleId = 1;
|
||||
|
||||
replicationService = fixture.debugElement.injector.get(ReplicationService);
|
||||
|
||||
|
||||
endpointService = fixtureCreate.debugElement.injector.get(EndpointService);
|
||||
|
||||
spyRules = spyOn(replicationService, 'getReplicationRules').and.returnValues(Promise.resolve(mockRules));
|
||||
spyJobs = spyOn(replicationService, 'getJobs').and.returnValues(Promise.resolve(mockJob));
|
||||
|
||||
|
||||
spyEndpoint = spyOn(endpointService, 'getEndpoints').and.returnValues(Promise.resolve(mockEndpoints));
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(()=>{
|
||||
fixture.detectChanges();
|
||||
@ -221,6 +265,7 @@ describe('Replication Component (inline template)', ()=>{
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('Should load replication rules', async(()=>{
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(()=>{
|
||||
|
@ -12,8 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
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';
|
||||
|
||||
@ -23,7 +21,7 @@ import { ErrorHandler } from '../error-handler/error-handler';
|
||||
|
||||
import { ReplicationService } from '../service/replication.service';
|
||||
import { RequestQueryParams } from '../service/RequestQueryParams';
|
||||
import { ReplicationRule, ReplicationJob, Endpoint, ReplicationJobItem } from '../service/interface';
|
||||
import { ReplicationRule, ReplicationJob, ReplicationJobItem } from '../service/interface';
|
||||
|
||||
import {
|
||||
toPromise,
|
||||
@ -89,13 +87,14 @@ export class SearchOption {
|
||||
export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() projectId: number | string;
|
||||
@Input() projectName: string;
|
||||
@Input() isSystemAdmin: boolean;
|
||||
@Input() withReplicationJob: boolean;
|
||||
@Input() readonly: boolean;
|
||||
|
||||
@Output() redirect = new EventEmitter<ReplicationRule>();
|
||||
@Output() openCreateRule = new EventEmitter<any>();
|
||||
@Output() openEdit = new EventEmitter<string | number>();
|
||||
@Output() goToRegistry = new EventEmitter<any>();
|
||||
|
||||
search: SearchOption = new SearchOption();
|
||||
|
||||
@ -106,7 +105,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
currentJobStatus: { key: string, description: string };
|
||||
|
||||
changedRules: ReplicationRule[];
|
||||
initSelectedId: number | string;
|
||||
|
||||
rules: ReplicationRule[];
|
||||
loading: boolean;
|
||||
@ -121,8 +119,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(ListReplicationRuleComponent)
|
||||
listReplicationRule: ListReplicationRuleComponent;
|
||||
|
||||
/* @ViewChild(CreateEditRuleComponent)
|
||||
createEditPolicyComponent: CreateEditRuleComponent;*/
|
||||
@ViewChild(CreateEditRuleComponent)
|
||||
createEditPolicyComponent: CreateEditRuleComponent;
|
||||
|
||||
@ViewChild("replicationLogViewer")
|
||||
replicationLogViewer: JobLogViewerComponent;
|
||||
@ -164,20 +162,22 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
// open replication rule
|
||||
openModal(): void {
|
||||
this.openCreateRule.emit();
|
||||
this.createEditPolicyComponent.openCreateEditRule();
|
||||
}
|
||||
|
||||
// edit replication rule
|
||||
openEditRule(rule: ReplicationRule) {
|
||||
if (rule) {
|
||||
let editable = true;
|
||||
if (rule.enabled === 1) {
|
||||
editable = false;
|
||||
}
|
||||
this.openEdit.emit(rule.id);
|
||||
this.createEditPolicyComponent.openCreateEditRule(rule.id);
|
||||
}
|
||||
}
|
||||
|
||||
goRegistry(): void {
|
||||
this.goToRegistry.emit();
|
||||
}
|
||||
|
||||
//Server driven data loading
|
||||
clrLoadJobs(state: State): void {
|
||||
if (!state || !state.page || !this.search.ruleId) {
|
||||
@ -209,6 +209,12 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
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
|
||||
.getJobs(this.search.ruleId, params))
|
||||
.then(
|
||||
@ -382,6 +388,9 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
|
||||
|
||||
refreshJobs() {
|
||||
this.currentJobStatus = this.jobStatus[0];
|
||||
this.search.startTime = ' ';
|
||||
this.search.endTime = ' ';
|
||||
this.search.repoName = "";
|
||||
this.search.startTimestamp = "";
|
||||
this.search.endTimestamp = "";
|
||||
|
@ -1,3 +1,4 @@
|
||||
import {Project} from "../project-policy-config/project";
|
||||
/**
|
||||
* The base interface contains the general properties
|
||||
*
|
||||
@ -83,18 +84,40 @@ export interface Endpoint extends Base {
|
||||
*
|
||||
* @export
|
||||
* @interface ReplicationRule
|
||||
* @interface Filter
|
||||
* @interface Trigger
|
||||
*/
|
||||
export interface ReplicationRule extends Base {
|
||||
project_id: number | string;
|
||||
project_name: string;
|
||||
target_id: number | string;
|
||||
target_name: string;
|
||||
enabled: number;
|
||||
description?: string;
|
||||
cron_str?: string;
|
||||
start_time?: Date;
|
||||
error_job_count?: number;
|
||||
deleted: number;
|
||||
[key: string]: any;
|
||||
id?: number;
|
||||
name: string;
|
||||
description: string;
|
||||
projects: Project[];
|
||||
targets: Endpoint[] ;
|
||||
trigger: Trigger ;
|
||||
filters: Filter[] ;
|
||||
replicate_existing_image_now?: boolean;
|
||||
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
|
||||
*/
|
||||
export interface ReplicationJobItem extends Base {
|
||||
[key: string]: any | any[]
|
||||
[key: string]: any | any[];
|
||||
status: string;
|
||||
repository: string;
|
||||
policy_id: number;
|
||||
@ -151,7 +174,7 @@ export interface AccessLog {
|
||||
* @interface AccessLogItem
|
||||
*/
|
||||
export interface AccessLogItem {
|
||||
[key: string]: any | any[]
|
||||
[key: string]: any | any[];
|
||||
log_id: number;
|
||||
project_id: number;
|
||||
repo_name: string;
|
||||
|
@ -6,7 +6,8 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
|
||||
import { Project } from '../project-policy-config/project';
|
||||
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.
|
||||
@ -38,6 +39,8 @@ export abstract class ProjectService {
|
||||
* @memberOf EndpointService
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
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 {
|
||||
return this.http
|
||||
.put(`/api/projects/${projectId}`, { 'metadata': {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { RequestQueryParams } from './RequestQueryParams';
|
||||
import { ReplicationJob, ReplicationRule, ReplicationJobItem } from './interface';
|
||||
import {ReplicationJob, ReplicationRule, ReplicationJobItem} from './interface';
|
||||
import { Injectable, Inject } from "@angular/core";
|
||||
import 'rxjs/add/observable/of';
|
||||
import { Http, RequestOptions } from '@angular/http';
|
||||
@ -62,7 +62,7 @@ export abstract class 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.
|
||||
@ -159,7 +159,7 @@ export class ReplicationDefaultService extends ReplicationService {
|
||||
//Private methods
|
||||
//Check if the rule object is valid
|
||||
_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[] {
|
||||
@ -177,7 +177,7 @@ export class ReplicationDefaultService extends ReplicationService {
|
||||
|
||||
return this.http.get(this._ruleBaseUrl, buildHttpRequestOptions(queryParams)).toPromise()
|
||||
.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 {
|
||||
@ -201,13 +201,13 @@ export class ReplicationDefaultService extends ReplicationService {
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
public updateReplicationRule(replicationRule: ReplicationRule): Observable<any> | Promise<any> | any {
|
||||
if (!this._isValidRule(replicationRule) || !replicationRule.id) {
|
||||
public updateReplicationRule(id: number, rep: ReplicationRule): Observable<any> | Promise<any> | any {
|
||||
if (!this._isValidRule(rep)) {
|
||||
return Promise.reject('Bad argument');
|
||||
}
|
||||
|
||||
let url: string = `${this._ruleBaseUrl}/${replicationRule.id}`;
|
||||
return this.http.put(url, JSON.stringify(replicationRule), HTTP_JSON_OPTIONS).toPromise()
|
||||
let url = `${this._ruleBaseUrl}/${id}`;
|
||||
return this.http.put(url, JSON.stringify(rep), HTTP_JSON_OPTIONS).toPromise()
|
||||
.then(response => response)
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
@ -298,7 +298,7 @@ export class ReplicationDefaultService extends ReplicationService {
|
||||
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()
|
||||
.then(response => response.text())
|
||||
.catch(error => Promise.reject(error));
|
||||
|
@ -2,8 +2,8 @@ import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpModule, Http } from '@angular/http';
|
||||
import { ClarityModule } from 'clarity-angular';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from '@ngx-translate/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { TranslateModule, TranslateLoader, MissingTranslationHandler } from '@ngx-translate/core';
|
||||
import { MyMissingTranslationHandler } from '../i18n/missing-trans.handler';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { TranslatorJsonLoader } from '../i18n/local-json.loader';
|
||||
@ -41,6 +41,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
|
||||
CommonModule,
|
||||
HttpModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ClipboardModule,
|
||||
CookieModule.forRoot(),
|
||||
ClarityModule.forRoot(),
|
||||
@ -60,6 +61,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
|
||||
CommonModule,
|
||||
HttpModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CookieModule,
|
||||
ClipboardModule,
|
||||
ClarityModule,
|
||||
|
@ -31,7 +31,7 @@
|
||||
"clarity-icons": "^0.10.17",
|
||||
"clarity-ui": "^0.10.17",
|
||||
"core-js": "^2.4.1",
|
||||
"harbor-ui": "0.6.45",
|
||||
"harbor-ui": "0.6.47",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
|
@ -50,8 +50,6 @@ import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deac
|
||||
import { MemberGuard } from './shared/route/member-guard-activate.service';
|
||||
|
||||
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';
|
||||
|
||||
const harborRoutes: Routes = [
|
||||
@ -92,20 +90,6 @@ const harborRoutes: Routes = [
|
||||
canActivate: [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',
|
||||
component: TagRepositoryComponent,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user