diff --git a/.travis.yml b/.travis.yml
index 02101345b..536583496 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -77,7 +77,7 @@ script:
- sudo mkdir -p /etc/ui/ca/
- sudo mv ./tests/ca.crt /etc/ui/ca/
- sudo mkdir -p /harbor
- - sudo mv ./VERSION /harbor/VERSION
+ - sudo mv ./VERSION /harbor/UIVERSION
- sudo service mysql stop
- sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0
- cat ./src/ui_ng/lib/npm-ut-test-results
diff --git a/Makefile b/Makefile
index edca0d685..1de8c1477 100644
--- a/Makefile
+++ b/Makefile
@@ -85,9 +85,14 @@ BUILDBIN=false
MIGRATORFLAG=false
# version prepare
+# for docker image tag
VERSIONTAG=dev
+# for harbor package name
+PKGVERSIONTAG=dev
+# for harbor about dialog
+UIVERSIONTAG=dev
VERSIONFILEPATH=$(CURDIR)
-VERSIONFILENAME=VERSION
+VERSIONFILENAME=UIVERSION
#versions
REGISTRYVERSION=v2.6.2
@@ -205,13 +210,13 @@ DOCKERSAVE_PARA=$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
vmware/nginx-photon:$(NGINXVERSION) vmware/registry-photon:$(REGISTRYVERSION)-$(VERSIONTAG) \
vmware/photon:$(PHOTONVERSION)
-PACKAGE_OFFLINE_PARA=-zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
+PACKAGE_OFFLINE_PARA=-zcvf harbor-offline-installer-$(PKGVERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tar.gz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/ha
-PACKAGE_ONLINE_PARA=-zcvf harbor-online-installer-$(VERSIONTAG).tgz \
+PACKAGE_ONLINE_PARA=-zcvf harbor-online-installer-$(PKGVERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/prepare \
$(HARBORPKG)/LICENSE $(HARBORPKG)/NOTICE \
$(HARBORPKG)/install.sh $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
@@ -236,7 +241,7 @@ ifeq ($(MIGRATORFLAG), true)
endif
version:
- @printf $(VERSIONTAG) > $(VERSIONFILEPATH)/$(VERSIONFILENAME);
+ @printf $(UIVERSIONTAG) > $(VERSIONFILEPATH)/$(VERSIONFILENAME);
check_environment:
@$(MAKEPATH)/$(CHECKENVCMD)
diff --git a/VERSION b/VERSION
index 90012116c..76864c1c2 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-dev
\ No newline at end of file
+v1.5.0
\ No newline at end of file
diff --git a/contrib/helm/harbor/.gitignore b/contrib/helm/harbor/.gitignore
new file mode 100644
index 000000000..e6b3ed643
--- /dev/null
+++ b/contrib/helm/harbor/.gitignore
@@ -0,0 +1 @@
+charts/*
diff --git a/contrib/helm/harbor/Chart.yaml b/contrib/helm/harbor/Chart.yaml
index 55fa4b477..3244c10d6 100644
--- a/contrib/helm/harbor/Chart.yaml
+++ b/contrib/helm/harbor/Chart.yaml
@@ -1,5 +1,5 @@
name: harbor
-version: 0.1.0
+version: 0.1.1
appVersion: 1.4.0
description: An Enterprise-class Docker Registry by VMware
keywords:
diff --git a/contrib/helm/harbor/README.md b/contrib/helm/harbor/README.md
index 24ef9ad35..52da97939 100644
--- a/contrib/helm/harbor/README.md
+++ b/contrib/helm/harbor/README.md
@@ -152,8 +152,8 @@ The following tables lists the configurable parameters of the Harbor chart and t
| `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.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | `clair.pgResources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined |
+| `clair.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined
+| `postgresql` | Overrides for postgresql chart [values.yaml](https://github.com/kubernetes/charts/blob/f2938a46e3ae8e2512ede1142465004094c3c333/stable/postgresql/values.yaml) | see values.yaml
| | | |
diff --git a/contrib/helm/harbor/requirements.lock b/contrib/helm/harbor/requirements.lock
new file mode 100644
index 000000000..fa6b0c773
--- /dev/null
+++ b/contrib/helm/harbor/requirements.lock
@@ -0,0 +1,6 @@
+dependencies:
+- name: postgresql
+ repository: https://kubernetes-charts.storage.googleapis.com
+ version: 0.9.1
+digest: sha256:e89ecacdca0cc0414763a586832bf7ca3d57bd25ac8e1a08e41080b610eb5a7d
+generated: 2018-03-09T15:34:27.167977722-06:00
diff --git a/contrib/helm/harbor/requirements.yaml b/contrib/helm/harbor/requirements.yaml
new file mode 100644
index 000000000..a6a999960
--- /dev/null
+++ b/contrib/helm/harbor/requirements.yaml
@@ -0,0 +1,4 @@
+dependencies:
+- name: postgresql
+ version: 0.9.1
+ repository: https://kubernetes-charts.storage.googleapis.com
diff --git a/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml b/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml
index 559f65e22..1ff8f8b8b 100644
--- a/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml
+++ b/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml
@@ -50,10 +50,10 @@ data:
ADMIRAL_URL: "NA"
RESET: "false"
WITH_CLAIR: "{{ .Values.clair.enabled }}"
- CLAIR_DB_HOST: "{{ template "harbor.fullname" . }}-clair-pg"
+ CLAIR_DB_HOST: "{{ .Release.Name }}-postgresql"
CLAIR_DB_PORT: "5432"
- CLAIR_DB: "postgres"
- CLAIR_DB_USERNAME: "postgres"
+ CLAIR_DB: "{{ .Values.clair.postgresDatabase }}"
+ CLAIR_DB_USERNAME: "{{ .Values.clair.postgresUser }}"
CLAIR_DB_PASSWORD: "{{ .Values.clair.postgresPassword }}"
UAA_ENDPOINT: ""
UAA_CLIENTID: ""
diff --git a/contrib/helm/harbor/templates/clair/clair-cm.yaml b/contrib/helm/harbor/templates/clair/clair-cm.yaml
index 96a617e95..e04bf2526 100644
--- a/contrib/helm/harbor/templates/clair/clair-cm.yaml
+++ b/contrib/helm/harbor/templates/clair/clair-cm.yaml
@@ -2,7 +2,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
- name: {{ template "harbor.fullname" . }}
+ name: {{ template "harbor.fullname" . }}-clair
labels:
{{ include "harbor.labels" . | indent 4 }}
component: clair
@@ -12,8 +12,7 @@ data:
database:
type: pgsql
options:
- source: "postgresql://postgres:{{ .Values.clair.postgresPassword }}@{{ template "harbor.fullname" . }}-clair-pg:5432?sslmode=disable"
-
+ source: "postgresql://{{ .Values.clair.postgresUser }}:{{ .Values.clair.postgresPassword }}@{{ .Release.Name }}-postgresql:5432/{{ .Values.clair.postgresDatabase }}?sslmode=disable"
# Number of elements kept in the cache
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
cachesize: 16384
diff --git a/contrib/helm/harbor/templates/clair/clair-dpl.yaml b/contrib/helm/harbor/templates/clair/clair-dpl.yaml
index f10ec6a9c..60905e58a 100644
--- a/contrib/helm/harbor/templates/clair/clair-dpl.yaml
+++ b/contrib/helm/harbor/templates/clair/clair-dpl.yaml
@@ -34,7 +34,7 @@ spec:
volumes:
- name: clair-config
configMap:
- name: "{{ template "harbor.fullname" . }}"
+ name: "{{ template "harbor.fullname" . }}-clair"
items:
- key: config.yaml
path: config.yaml
diff --git a/contrib/helm/harbor/templates/clair/postgres-secret.yaml b/contrib/helm/harbor/templates/clair/postgres-secret.yaml
deleted file mode 100644
index efa7f6996..000000000
--- a/contrib/helm/harbor/templates/clair/postgres-secret.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-{{ if .Values.clair.enabled }}
-apiVersion: v1
-kind: Secret
-metadata:
- name: {{ template "harbor.fullname" . }}-clair-pg-config
- labels:
-{{ include "harbor.labels" . | indent 4 }}
-type: Opaque
-data:
- POSTGRES_PASSWORD: {{ .Values.clair.postgresPassword | b64enc | quote }}
-{{ end }}
\ No newline at end of file
diff --git a/contrib/helm/harbor/templates/clair/postgres-ss.yaml b/contrib/helm/harbor/templates/clair/postgres-ss.yaml
deleted file mode 100644
index d47721354..000000000
--- a/contrib/helm/harbor/templates/clair/postgres-ss.yaml
+++ /dev/null
@@ -1,72 +0,0 @@
-{{ if .Values.clair.enabled }}
-apiVersion: apps/v1beta2
-kind: StatefulSet
-metadata:
- name: {{ template "harbor.fullname" . }}-clair-pg
- labels:
-{{ include "harbor.labels" . | indent 4 }}
- component: clair-pg
-spec:
- serviceName: "{{ template "harbor.fullname" . }}-clair-pg"
- selector:
- matchLabels:
-{{ include "harbor.matchLabels" . | indent 6 }}
- component: clair-pg
- template:
- metadata:
- name: {{ template "harbor.fullname" . }}-clair-pg
- labels:
-{{ include "harbor.labels" . | indent 8 }}
- component: clair-pg
- spec:
- containers:
- - name: postgres
- image: {{ .Values.clair.pgImage.repository }}:{{ .Values.clair.pgImage.tag }}
- imagePullPolicy: {{ .Values.clair.pgImage.pullPolicy }}
- resources:
-{{ toYaml .Values.clair.pgResources | indent 10 }}
- env:
- - name: POSTGRES_PASSWORD
- valueFrom:
- secretKeyRef:
- name: {{ template "harbor.fullname" . }}-clair-pg-config
- key: POSTGRES_PASSWORD
- resources:
- limits:
- cpu: 1000m
- memory: 1Gi
- requests:
- cpu: 100m
- memory: 512Mi
- volumeMounts:
- - name: pgdata
- mountPath: /var/lib/postgresql
- ports:
- - containerPort: 5432
- name: postgres-port
- protocol: TCP
-{{- if not .Values.persistence.enabled }}
- volumes:
- - name: pgdata
- emptyDir: {}
-{{- end }}
- {{- if .Values.persistence.enabled }}
- volumeClaimTemplates:
- - metadata:
- name: pgdata
- labels:
-{{ include "harbor.labels" . | indent 8 }}
- spec:
- accessModes: [{{ .Values.clair.volumes.pgData.accessMode | quote }}]
- {{- if .Values.clair.volumes.pgData.storageClass }}
- {{- if (eq "-" .Values.clair.volumes.pgData.storageClass) }}
- storageClassName: ""
- {{- else }}
- storageClassName: "{{ .Values.clair.volumes.pgData.storageClass }}"
- {{- end }}
- {{- end }}
- resources:
- requests:
- storage: {{ .Values.clair.volumes.pgData.size | quote }}
- {{- end -}}
-{{- end -}}
\ No newline at end of file
diff --git a/contrib/helm/harbor/templates/clair/postgres-svc.yaml b/contrib/helm/harbor/templates/clair/postgres-svc.yaml
deleted file mode 100644
index 2944fd48a..000000000
--- a/contrib/helm/harbor/templates/clair/postgres-svc.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-{{ if .Values.clair.enabled }}
-apiVersion: v1
-kind: Service
-metadata:
- name: {{ template "harbor.fullname" . }}-clair-pg
- labels:
-{{ include "harbor.labels" . | indent 4 }}
-spec:
- ports:
- - port: 5432
- selector:
-{{ include "harbor.matchLabels" . | indent 4 }}
- component: clair-pg
-{{ end }}
\ No newline at end of file
diff --git a/contrib/helm/harbor/templates/registry/registry-ss.yaml b/contrib/helm/harbor/templates/registry/registry-ss.yaml
index 0f961fece..663394dd2 100644
--- a/contrib/helm/harbor/templates/registry/registry-ss.yaml
+++ b/contrib/helm/harbor/templates/registry/registry-ss.yaml
@@ -61,7 +61,7 @@ spec:
volumeClaimTemplates:
- metadata:
name: "registry-data"
- labels:
+ labels:
{{ include "harbor.labels" . | indent 8 }}
spec:
accessModes: [{{ .Values.registry.volumes.data.accessMode | quote }}]
@@ -76,4 +76,4 @@ spec:
requests:
storage: {{ .Values.registry.volumes.data.size | quote }}
{{- end -}}
- {{- end -}}
\ No newline at end of file
+ {{- end -}}
diff --git a/contrib/helm/harbor/values.yaml b/contrib/helm/harbor/values.yaml
index d53d5f1ab..737bf2b74 100644
--- a/contrib/helm/harbor/values.yaml
+++ b/contrib/helm/harbor/values.yaml
@@ -243,20 +243,18 @@ registry:
# memory: 256Mi
# cpu: 100m
-## Clair support is not yet fully implemented in the Helm Charts
-## Enabling it will just break things.
-#
clair:
enabled: true
image:
repository: vmware/clair-photon
tag: v2.0.1-v1.4.0
pullPolicy: IfNotPresent
+## The following needs to match the credentials
+## in the `postgresql` configuration under the
+## `postgresql` namespace below.
postgresPassword: not-a-secure-password
- pgImage:
- repository: postgres
- tag: "9.6.4"
- pullPolicy: IfNotPresent
+ postgresUser: clair
+ postgresDatabase: clair
# resources:
# requests:
# memory: 256Mi
@@ -280,3 +278,13 @@ clair:
#
notary:
enabled: false
+
+## Settings for postgresql dependency.
+## see https://github.com/kubernetes/charts/tree/master/stable/postgresql
+## for further configurables.
+postgresql:
+ postgresUser: clair
+ postgresPassword: not-a-secure-password
+ postgresDatabase: clair
+ persistence:
+ enabled: false
diff --git a/docs/compile_guide.md b/docs/compile_guide.md
index f5ab6ffba..6d6090aad 100644
--- a/docs/compile_guide.md
+++ b/docs/compile_guide.md
@@ -44,25 +44,25 @@ You can compile the code by one of the three approaches:
* Get offcial Golang image from docker hub:
```sh
- $ docker pull golang:1.7.3
+ $ docker pull golang:1.9.2
```
* Build, install and bring up Harbor without Notary:
```sh
- $ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.7
+ $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0
```
* Build, install and bring up Harbor with Notary:
```sh
- $ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.7 NOTARYFLAG=true
+ $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0 NOTARYFLAG=true
```
* Build, install and bring up Harbor with Clair:
```sh
- $ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.7 CLAIRFLAG=true
+ $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0 CLAIRFLAG=true
```
#### II. Compile code with your own Golang environment, then build Harbor
@@ -118,6 +118,8 @@ REGISTRYSERVER | Remote registry server IP address
REGISTRYUSER | Remote registry server user name
REGISTRYPASSWORD | Remote registry server user password
REGISTRYPROJECTNAME| Project name on remote registry server
+VERSIONTAG | Harbor images tag, default: dev
+PKGVERSIONTAG | Harbor online and offline version tag, default:dev
* Predefined targets:
diff --git a/docs/kubernetes_deployment.md b/docs/kubernetes_deployment.md
index f2c5e36d6..d9527d425 100644
--- a/docs/kubernetes_deployment.md
+++ b/docs/kubernetes_deployment.md
@@ -1,6 +1,7 @@
+**IMPORTANT** This guide is deprecated and not updated any more. We strongly recommend using [Harbor Helm Chart](https://github.com/vmware/harbor/tree/master/contrib/helm/harbor) to deploy latest Harbor release on Kubernetes.
## Integration with Kubernetes
-This Document decribes how to deploy Harbor on Kubernetes. It has been verified on **Kubernetes v1.6.5** and **Harbor v1.2.0**
+This Document decribes how to deploy Harbor on Kubernetes. It has been verified on **Kubernetes v1.6.5** and **Harbor v1.2.0**
### Prerequisite
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index 682a59128..e44d3223c 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -906,6 +906,11 @@ paths:
type: string
required: false
description: Repo name for filtering results.
+ - name: label_id
+ in: query
+ type: integer
+ required: false
+ description: The ID of label used to filter the result.
- name: page
in: query
type: integer
@@ -1144,6 +1149,11 @@ paths:
type: string
required: true
description: Relevant repository name.
+ - name: label_ids
+ in: query
+ type: string
+ required: false
+ description: A list of comma separated label IDs.
tags:
- Products
responses:
@@ -2464,7 +2474,7 @@ paths:
'403':
description: Only admin has this authority.
'415':
- $ref: '#responses/UnsupportedMediaType'
+ $ref: '#/responses/UnsupportedMediaType'
'500':
description: Unexpected internal errors.
responses:
@@ -3346,6 +3356,10 @@ definitions:
description: >-
This attribute restricts what users have the permission to create
project. It can be "everyone" or "adminonly".
+ read_only:
+ type: boolean
+ description: >-
+ 'docker push' is prohibited by Harbor if you set it to true.
self_registration:
type: boolean
description: >-
diff --git a/make/common/templates/adminserver/env b/make/common/templates/adminserver/env
index 9a5fabfac..cc1040fb0 100644
--- a/make/common/templates/adminserver/env
+++ b/make/common/templates/adminserver/env
@@ -56,3 +56,4 @@ UAA_VERIFY_CERT=$uaa_verify_cert
UI_URL=http://ui:8080
JOBSERVICE_URL=http://jobservice:8080
REGISTRY_STORAGE_PROVIDER_NAME=$storage_provider_name
+READ_ONLY=false
diff --git a/make/dev/ui/Dockerfile b/make/dev/ui/Dockerfile
index 046b047ea..ac4978ef1 100644
--- a/make/dev/ui/Dockerfile
+++ b/make/dev/ui/Dockerfile
@@ -12,7 +12,7 @@ COPY src/ui/static /go/bin/static
COPY src/favicon.ico /go/bin/favicon.ico
RUN mkdir /go/bin/harbor/
-COPY VERSION /go/bin/harbor/VERSION
+COPY VERSION /go/bin/harbor/UIVERSION
RUN chmod u+x /go/bin/harbor_ui
diff --git a/make/harbor.cfg b/make/harbor.cfg
index 3a1ab6b67..738fccdca 100644
--- a/make/harbor.cfg
+++ b/make/harbor.cfg
@@ -1,5 +1,7 @@
## Configuration file of Harbor
+#This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY!
+_version = 1.5.0
#The IP address or hostname to access admin UI and registry service.
#DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
hostname = reg.mydomain.com
diff --git a/make/photon/db/registry.sql b/make/photon/db/registry.sql
index 02e96d663..b59694c27 100644
--- a/make/photon/db/registry.sql
+++ b/make/photon/db/registry.sql
@@ -277,8 +277,9 @@ create table harbor_resource_label (
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,
+ resource_id int,
+# the resource_name is the name of image when the resource_type is i
+ resource_name varchar(256),
# 'p' for project
# 'r' for repository
# 'i' for image
@@ -286,7 +287,7 @@ create table harbor_resource_label (
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)
+ CONSTRAINT unique_label_resource UNIQUE (label_id,resource_id, resource_name, resource_type)
);
CREATE TABLE IF NOT EXISTS `alembic_version` (
diff --git a/make/photon/db/registry_sqlite.sql b/make/photon/db/registry_sqlite.sql
index 5d045edb0..40490a4f7 100644
--- a/make/photon/db/registry_sqlite.sql
+++ b/make/photon/db/registry_sqlite.sql
@@ -267,9 +267,12 @@ create table harbor_resource_label (
/*
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,
+ resource_id int,
+/*
+ the resource_name is the name of image when the resource_type is i
+*/
+ resource_name varchar(256),
/*
'p' for project
'r' for repository
@@ -278,7 +281,7 @@ create table harbor_resource_label (
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)
+ UNIQUE (label_id,resource_id,resource_name,resource_type)
);
create table alembic_version (
diff --git a/make/photon/registry/entrypoint.sh b/make/photon/registry/entrypoint.sh
index 873f62001..690a399f0 100644
--- a/make/photon/registry/entrypoint.sh
+++ b/make/photon/registry/entrypoint.sh
@@ -7,10 +7,16 @@ if [ -d /etc/registry ]; then
fi
if [ -d /var/lib/registry ]; then
chown 10000:10000 -R /var/lib/registry
-fi
+fi
if [ -d /storage ]; then
- chown 10000:10000 -R /storage
-fi
+ if ! stat -c '%u:%g' /storage | grep -q '10000:10000' ; then
+ # 10000 is the id of harbor user/group.
+ # Usually NFS Server does not allow changing owner of the export directory,
+ # so need to skip this step and requires NFS Server admin to set its owner to 10000.
+ chown 10000:10000 -R /storage
+ fi
+fi
+
case "$1" in
*.yaml|*.yml) set -- registry serve "$@" ;;
serve|garbage-collect|help|-*) set -- registry "$@" ;;
diff --git a/make/photon/ui/Dockerfile b/make/photon/ui/Dockerfile
index 12c7acd78..7300a1e15 100644
--- a/make/photon/ui/Dockerfile
+++ b/make/photon/ui/Dockerfile
@@ -1,6 +1,6 @@
FROM vmware/photon:1.0
-RUN tdnf distro-sync -y || echo \
+RUN tdnf distro-sync -y \
&& tdnf erase vim -y \
&& tdnf install sudo -y >> /dev/null\
&& tdnf clean all \
@@ -8,7 +8,7 @@ RUN tdnf distro-sync -y || echo \
&& mkdir /harbor/
HEALTHCHECK CMD curl -s -o /dev/null -w "%{http_code}" 127.0.0.1:8080/api/systeminfo|grep 200
-COPY ./make/dev/ui/harbor_ui ./src/favicon.ico ./make/photon/ui/start.sh ./VERSION /harbor/
+COPY ./make/dev/ui/harbor_ui ./src/favicon.ico ./make/photon/ui/start.sh ./UIVERSION /harbor/
COPY ./src/ui/views /harbor/views
COPY ./src/ui/static /harbor/static
diff --git a/src/Gopkg.lock b/src/Gopkg.lock
index 749c1ebda..e9d84757c 100644
--- a/src/Gopkg.lock
+++ b/src/Gopkg.lock
@@ -124,8 +124,8 @@
[[projects]]
name = "github.com/go-sql-driver/mysql"
packages = ["."]
- revision = "a732e14c62dde3285440047bba97581bc472ae18"
- version = "v1.2"
+ revision = "a0583e0143b1624142adab07e0e97fe106d99561"
+ version = "v1.3"
[[projects]]
name = "github.com/gocraft/work"
@@ -152,25 +152,29 @@
[[projects]]
name = "github.com/gorilla/handlers"
packages = ["."]
- revision = "13d73096a474cac93275c679c7b8a2dc17ddba82"
+ revision = "90663712d74cb411cbef281bc1e08c19d1a76145"
+ version = "v1.3.0"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
- revision = "780415097119f6f61c55475fe59b66f3c3e9ea53"
+ revision = "7f08801859139f86dfafd1c296e2cba9a80d292e"
+ version = "v1.6.0"
[[projects]]
+ branch = "master"
name = "github.com/lib/pq"
packages = [
".",
"oid"
]
- revision = "dd1fe2071026ce53f36a39112e645b4d4f5793a4"
+ revision = "b2004221932bd6b13167ef654c81cffac36f7537"
[[projects]]
name = "github.com/mattn/go-sqlite3"
packages = ["."]
- revision = "3fb7a0e792edd47bf0cf1e919dfc14e2be412e15"
+ revision = "6c771bb9887719704b210e87e934f08be014bdb1"
+ version = "v1.6.0"
[[projects]]
name = "github.com/miekg/pkcs11"
diff --git a/src/Gopkg.toml b/src/Gopkg.toml
index ecdf74926..22cc81e07 100644
--- a/src/Gopkg.toml
+++ b/src/Gopkg.toml
@@ -42,7 +42,11 @@ ignored = ["github.com/vmware/harbor/tests*"]
[[constraint]]
name = "github.com/go-sql-driver/mysql"
- version = "=1.2.0"
+ version = "=1.3.0"
+
+[[constraint]]
+ name = "github.com/mattn/go-sqlite3"
+ version = "=1.6.0"
[[constraint]]
name = "github.com/opencontainers/go-digest"
@@ -54,4 +58,12 @@ ignored = ["github.com/vmware/harbor/tests*"]
[[constraint]]
name = "github.com/stretchr/testify"
- version = "=1.2.0"
\ No newline at end of file
+ version = "=1.2.0"
+
+[[constraint]]
+ name = "github.com/gorilla/handlers"
+ version = "=1.3.0"
+
+[[constraint]]
+ name = "github.com/gorilla/mux"
+ version = "=1.6.0"
\ No newline at end of file
diff --git a/src/adminserver/systemcfg/store/database/driver_db.go b/src/adminserver/systemcfg/store/database/driver_db.go
index 25d474bb3..fc520704b 100644
--- a/src/adminserver/systemcfg/store/database/driver_db.go
+++ b/src/adminserver/systemcfg/store/database/driver_db.go
@@ -48,6 +48,7 @@ var (
common.EmailInsecure: true,
common.LDAPVerifyCert: true,
common.UAAVerifyCert: true,
+ common.ReadOnly: true,
}
mapKeys = map[string]bool{
common.ScanAllPolicy: true,
diff --git a/src/adminserver/systemcfg/systemcfg.go b/src/adminserver/systemcfg/systemcfg.go
index d48bd66cb..ed5bce093 100644
--- a/src/adminserver/systemcfg/systemcfg.go
+++ b/src/adminserver/systemcfg/systemcfg.go
@@ -152,6 +152,10 @@ var (
common.UIURL: "UI_URL",
common.JobServiceURL: "JOBSERVICE_URL",
common.RegistryStorageProviderName: "REGISTRY_STORAGE_PROVIDER_NAME",
+ common.ReadOnly: &parser{
+ env: "READ_ONLY",
+ parse: parseStringToBool,
+ },
}
// configurations need read from environment variables
diff --git a/src/common/const.go b/src/common/const.go
index 1004714c1..59806e554 100644
--- a/src/common/const.go
+++ b/src/common/const.go
@@ -99,4 +99,5 @@ const (
RegistryStorageProviderName = "registry_storage_provider_name"
UserMember = "u"
GroupMember = "g"
+ ReadOnly = "read_only"
)
diff --git a/src/common/dao/repository.go b/src/common/dao/repository.go
index ee9533417..5eb7a9036 100644
--- a/src/common/dao/repository.go
+++ b/src/common/dao/repository.go
@@ -47,15 +47,6 @@ func GetRepositoryByName(name string) (*models.RepoRecord, error) {
return &r, err
}
-// GetAllRepositories ...
-func GetAllRepositories() ([]*models.RepoRecord, error) {
- o := GetOrmer()
- var repos []*models.RepoRecord
- _, err := o.QueryTable("repository").Limit(-1).
- OrderBy("Name").All(&repos)
- return repos, err
-}
-
// DeleteRepository ...
func DeleteRepository(name string) error {
o := GetOrmer()
@@ -94,18 +85,6 @@ func RepositoryExists(name string) bool {
return o.QueryTable("repository").Filter("name", name).Exist()
}
-// GetRepositoryByProjectName ...
-func GetRepositoryByProjectName(name string) ([]*models.RepoRecord, error) {
- sql := `select * from repository
- where project_id = (
- select project_id from project
- where name = ?
- )`
- repos := []*models.RepoRecord{}
- _, err := GetOrmer().Raw(sql, name).QueryRows(&repos)
- return repos, err
-}
-
//GetTopRepos returns the most popular repositories whose project ID is
// in projectIDs
func GetTopRepos(projectIDs []int64, n int) ([]*models.RepoRecord, error) {
@@ -124,46 +103,78 @@ func GetTopRepos(projectIDs []int64, n int) ([]*models.RepoRecord, error) {
}
// GetTotalOfRepositories ...
-func GetTotalOfRepositories(name string) (int64, error) {
- qs := GetOrmer().QueryTable(&models.RepoRecord{})
- if len(name) != 0 {
- qs = qs.Filter("Name__contains", name)
+func GetTotalOfRepositories(query ...*models.RepositoryQuery) (int64, error) {
+ sql, params := repositoryQueryConditions(query...)
+ sql = `select count(*) ` + sql
+ var total int64
+ if err := GetOrmer().Raw(sql, params).QueryRow(&total); err != nil {
+ return 0, err
}
- return qs.Count()
+ return total, nil
}
-// GetTotalOfRepositoriesByProject ...
-func GetTotalOfRepositoriesByProject(projectIDs []int64, name string) (int64, error) {
- if len(projectIDs) == 0 {
- return 0, nil
- }
-
- qs := GetOrmer().QueryTable(&models.RepoRecord{}).
- Filter("project_id__in", projectIDs)
-
- if len(name) != 0 {
- qs = qs.Filter("Name__contains", name)
- }
-
- return qs.Count()
-}
-
-// GetRepositoriesByProject ...
-func GetRepositoriesByProject(projectID int64, name string,
- limit, offset int64) ([]*models.RepoRecord, error) {
-
+// GetRepositories ...
+func GetRepositories(query ...*models.RepositoryQuery) ([]*models.RepoRecord, error) {
repositories := []*models.RepoRecord{}
- qs := GetOrmer().QueryTable(&models.RepoRecord{}).
- Filter("ProjectID", projectID)
-
- if len(name) != 0 {
- qs = qs.Filter("Name__contains", name)
+ sql, params := repositoryQueryConditions(query...)
+ sql = `select r.repository_id, r.name, r.project_id, r.description, r.pull_count,
+ r.star_count, r.creation_time, r.update_time ` + sql + `order by r.name `
+ if len(query) > 0 && query[0] != nil {
+ page, size := query[0].Page, query[0].Size
+ if size > 0 {
+ sql += `limit ? `
+ params = append(params, size)
+ if page > 0 {
+ sql += `offset ? `
+ params = append(params, size*(page-1))
+ }
+ }
}
- if limit > 0 {
- qs = qs.Limit(limit).Offset(offset)
- }
- _, err := qs.All(&repositories)
- return repositories, err
+ if _, err := GetOrmer().Raw(sql, params).QueryRows(&repositories); err != nil {
+ return nil, err
+ }
+ return repositories, nil
+}
+
+func repositoryQueryConditions(query ...*models.RepositoryQuery) (string, []interface{}) {
+ params := []interface{}{}
+ sql := `from repository r `
+ if len(query) == 0 || query[0] == nil {
+ return sql, params
+ }
+ q := query[0]
+ if len(q.ProjectName) > 0 {
+ sql += `join project p on r.project_id = p.project_id `
+ }
+
+ if q.LabelID > 0 {
+ sql += `join harbor_resource_label rl on r.repository_id = rl.resource_id
+ and rl.resource_type = 'r' `
+ }
+ sql += `where 1=1 `
+
+ if len(q.Name) > 0 {
+ sql += `and r.name like ? `
+ params = append(params, "%"+Escape(q.Name)+"%")
+ }
+
+ if len(q.ProjectIDs) > 0 {
+ sql += fmt.Sprintf(`and r.project_id in ( %s ) `,
+ paramPlaceholder(len(q.ProjectIDs)))
+ params = append(params, q.ProjectIDs)
+ }
+
+ if len(q.ProjectName) > 0 {
+ sql += `and p.name = ? `
+ params = append(params, q.ProjectName)
+ }
+
+ if q.LabelID > 0 {
+ sql += `and rl.label_id = ? `
+ params = append(params, q.LabelID)
+ }
+
+ return sql, params
}
diff --git a/src/common/dao/repository_test.go b/src/common/dao/repository_test.go
index 78379d85e..ac293d6f8 100644
--- a/src/common/dao/repository_test.go
+++ b/src/common/dao/repository_test.go
@@ -16,10 +16,11 @@ package dao
import (
"fmt"
- "strconv"
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/models"
)
@@ -32,61 +33,93 @@ var (
}
)
-func TestGetRepositoryByProjectName(t *testing.T) {
- if err := addRepository(repository); err != nil {
- t.Fatalf("failed to add repository %s: %v", name, err)
- }
- defer func() {
- if err := deleteRepository(name); err != nil {
- t.Fatalf("failed to delete repository %s: %v", name, err)
- }
- }()
+func TestGetTotalOfRepositories(t *testing.T) {
+ total, err := GetTotalOfRepositories()
+ require.Nil(t, err)
- repositories, err := GetRepositoryByProjectName(project)
- if err != nil {
- t.Fatalf("failed to get repositories of project %s: %v",
- project, err)
- }
+ err = addRepository(repository)
+ require.Nil(t, err)
+ defer deleteRepository(name)
- if len(repositories) == 0 {
- t.Fatal("unexpected length of repositories: 0, at least 1")
- }
+ n, err := GetTotalOfRepositories()
+ require.Nil(t, err)
+ assert.Equal(t, total+1, n)
+}
- exist := false
- for _, repo := range repositories {
- if repo.Name == name {
- exist = true
+func TestGetRepositories(t *testing.T) {
+ // no query
+ repositories, err := GetRepositories()
+ require.Nil(t, err)
+ n := len(repositories)
+
+ err = addRepository(repository)
+ require.Nil(t, err)
+ defer deleteRepository(name)
+
+ repositories, err = GetRepositories()
+ require.Nil(t, err)
+ assert.Equal(t, n+1, len(repositories))
+
+ // query by name
+ repositories, err = GetRepositories(&models.RepositoryQuery{
+ Name: name,
+ })
+ require.Nil(t, err)
+ require.Equal(t, 1, len(repositories))
+ assert.Equal(t, name, repositories[0].Name)
+
+ // query by project name
+ repositories, err = GetRepositories(&models.RepositoryQuery{
+ ProjectName: project,
+ })
+ require.Nil(t, err)
+ found := false
+ for _, repository := range repositories {
+ if repository.Name == name {
+ found = true
break
}
}
- if !exist {
- t.Errorf("there is no repository whose name is %s", name)
- }
-}
+ assert.True(t, found)
-func TestGetTotalOfRepositories(t *testing.T) {
- total, err := GetTotalOfRepositories("")
- if err != nil {
- t.Fatalf("failed to get total of repositoreis: %v", err)
- }
-
- if err := addRepository(repository); err != nil {
- t.Fatalf("failed to add repository %s: %v", name, err)
- }
- defer func() {
- if err := deleteRepository(name); err != nil {
- t.Fatalf("failed to delete repository %s: %v", name, err)
+ // query by project ID
+ repositories, err = GetRepositories(&models.RepositoryQuery{
+ ProjectIDs: []int64{1},
+ })
+ require.Nil(t, err)
+ found = false
+ for _, repository := range repositories {
+ if repository.Name == name {
+ found = true
+ break
}
- }()
-
- n, err := GetTotalOfRepositories("")
- if err != nil {
- t.Fatalf("failed to get total of repositoreis: %v", err)
}
+ assert.True(t, found)
- if n != total+1 {
- t.Errorf("unexpected total: %d != %d", n, total+1)
- }
+ // query by label ID
+ labelID, err := AddLabel(&models.Label{
+ Name: "label_for_test",
+ })
+ require.Nil(t, err)
+ defer DeleteLabel(labelID)
+
+ r, err := GetRepositoryByName(name)
+ require.Nil(t, err)
+
+ rlID, err := AddResourceLabel(&models.ResourceLabel{
+ LabelID: labelID,
+ ResourceID: r.RepositoryID,
+ ResourceType: common.ResourceTypeRepository,
+ })
+ require.Nil(t, err)
+ defer DeleteResourceLabel(rlID)
+
+ repositories, err = GetRepositories(&models.RepositoryQuery{
+ LabelID: labelID,
+ })
+ require.Nil(t, err)
+ require.Equal(t, 1, len(repositories))
+ assert.Equal(t, name, repositories[0].Name)
}
func TestGetTopRepos(t *testing.T) {
@@ -149,112 +182,6 @@ func TestGetTopRepos(t *testing.T) {
require.Equal(topRepos[0].Name, repository3.Name)
}
-func TestGetTotalOfRepositoriesByProject(t *testing.T) {
- var projectID int64 = 1
- repoName := "library/total_count"
-
- total, err := GetTotalOfRepositoriesByProject([]int64{projectID}, repoName)
- if err != nil {
- t.Errorf("failed to get total of repositoreis of project %d: %v", projectID, err)
- return
- }
-
- if err := addRepository(&models.RepoRecord{
- Name: repoName,
- ProjectID: projectID,
- }); err != nil {
- t.Errorf("failed to add repository %s: %v", repoName, err)
- return
- }
- defer func() {
- if err := deleteRepository(repoName); err != nil {
- t.Errorf("failed to delete repository %s: %v", name, err)
- return
- }
- }()
-
- n, err := GetTotalOfRepositoriesByProject([]int64{projectID}, repoName)
- if err != nil {
- t.Errorf("failed to get total of repositoreis of project %d: %v", projectID, err)
- return
- }
-
- if n != total+1 {
- t.Errorf("unexpected total: %d != %d", n, total+1)
- }
-}
-
-func TestGetRepositoriesByProject(t *testing.T) {
- var projectID int64 = 1
- repoName := "library/repository"
-
- if err := addRepository(&models.RepoRecord{
- Name: repoName,
- ProjectID: projectID,
- }); err != nil {
- t.Errorf("failed to add repository %s: %v", repoName, err)
- return
- }
- defer func() {
- if err := deleteRepository(repoName); err != nil {
- t.Errorf("failed to delete repository %s: %v", name, err)
- return
- }
- }()
-
- repositories, err := GetRepositoriesByProject(projectID, repoName, 10, 0)
- if err != nil {
- t.Errorf("failed to get repositoreis of project %d: %v", projectID, err)
- return
- }
-
- t.Log(repositories)
-
- for _, repository := range repositories {
- if repository.Name == repoName {
- return
- }
- }
-
- t.Errorf("repository %s not found", repoName)
-}
-
-func TestGetAllRepositories(t *testing.T) {
- require := require.New(t)
-
- var repos []*models.RepoRecord
- repos, err := GetAllRepositories()
- require.NoError(err)
- allBefore := len(repos)
-
- project1 := models.Project{
- OwnerID: 1,
- Name: "projectRepo",
- }
- var err2 error
- project1.ProjectID, err2 = AddProject(project1)
- require.NoError(err2)
-
- for i := 0; i < 1200; i++ {
- end := strconv.Itoa(i)
- repoRecord := models.RepoRecord{
- Name: "test" + end,
- ProjectID: project1.ProjectID,
- }
- err := AddRepository(repoRecord)
- require.NoError(err)
- }
-
- repos, err = GetAllRepositories()
- require.NoError(err)
- allAfter := len(repos)
-
- require.Equal(allAfter, allBefore+1200)
-
- err = clearRepositoryData()
- require.NoError(err)
-}
-
func addRepository(repository *models.RepoRecord) error {
return AddRepository(*repository)
}
diff --git a/src/common/dao/resource_label.go b/src/common/dao/resource_label.go
index f89bb27a5..56b17916b 100644
--- a/src/common/dao/resource_label.go
+++ b/src/common/dao/resource_label.go
@@ -29,14 +29,26 @@ func AddResourceLabel(rl *models.ResourceLabel) (int64, error) {
return GetOrmer().Insert(rl)
}
-// GetResourceLabel specified by ID
-func GetResourceLabel(rType, rID string, labelID int64) (*models.ResourceLabel, error) {
+// GetResourceLabel specified by resource ID or name
+// Get the ResourceLabel by ResourceID if rIDOrName is int
+// Get the ResourceLabel by ResourceName if rIDOrName is string
+func GetResourceLabel(rType string, rIDOrName interface{}, labelID int64) (*models.ResourceLabel, error) {
rl := &models.ResourceLabel{
ResourceType: rType,
- ResourceID: rID,
LabelID: labelID,
}
- if err := GetOrmer().Read(rl, "ResourceType", "ResourceID", "LabelID"); err != nil {
+
+ var err error
+ id, ok := rIDOrName.(int64)
+ if ok {
+ rl.ResourceID = id
+ err = GetOrmer().Read(rl, "ResourceType", "ResourceID", "LabelID")
+ } else {
+ rl.ResourceName = rIDOrName.(string)
+ err = GetOrmer().Read(rl, "ResourceType", "ResourceName", "LabelID")
+ }
+
+ if err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
@@ -47,13 +59,20 @@ func GetResourceLabel(rType, rID string, labelID int64) (*models.ResourceLabel,
}
// GetLabelsOfResource returns the label list of the resource
-func GetLabelsOfResource(rType, rID string) ([]*models.Label, error) {
+// Get the labels by ResourceID if rIDOrName is int, or get the labels by ResourceName
+func GetLabelsOfResource(rType string, rIDOrName interface{}) ([]*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 = ?`
+ where rl.resource_type = ? and`
+ if _, ok := rIDOrName.(int64); ok {
+ sql += ` rl.resource_id = ?`
+ } else {
+ sql += ` rl.resource_name = ?`
+ }
+
labels := []*models.Label{}
- _, err := GetOrmer().Raw(sql, rType, rID).QueryRows(&labels)
+ _, err := GetOrmer().Raw(sql, rType, rIDOrName).QueryRows(&labels)
return labels, err
}
@@ -65,10 +84,39 @@ func DeleteResourceLabel(id int64) error {
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()
+// DeleteLabelsOfResource removes all labels of the resource
+func DeleteLabelsOfResource(rType string, rIDOrName interface{}) error {
+ qs := GetOrmer().QueryTable(&models.ResourceLabel{}).
+ Filter("ResourceType", rType)
+ if _, ok := rIDOrName.(int64); ok {
+ qs = qs.Filter("ResourceID", rIDOrName)
+ } else {
+ qs = qs.Filter("ResourceName", rIDOrName)
+ }
+ _, err := qs.Delete()
return err
}
+
+// ListResourceLabels lists ResourceLabel according to the query conditions
+func ListResourceLabels(query ...*models.ResourceLabelQuery) ([]*models.ResourceLabel, error) {
+ qs := GetOrmer().QueryTable(&models.ResourceLabel{})
+ if len(query) > 0 {
+ q := query[0]
+ if q.LabelID > 0 {
+ qs = qs.Filter("LabelID", q.LabelID)
+ }
+ if len(q.ResourceType) > 0 {
+ qs = qs.Filter("ResourceType", q.ResourceType)
+ }
+ if q.ResourceID > 0 {
+ qs = qs.Filter("ResourceID", q.ResourceID)
+ }
+ if len(q.ResourceName) > 0 {
+ qs = qs.Filter("ResourceName", q.ResourceName)
+ }
+ }
+
+ rls := []*models.ResourceLabel{}
+ _, err := qs.All(&rls)
+ return rls, err
+}
diff --git a/src/common/dao/resource_label_test.go b/src/common/dao/resource_label_test.go
index 33259e1fd..572bec8b5 100644
--- a/src/common/dao/resource_label_test.go
+++ b/src/common/dao/resource_label_test.go
@@ -32,7 +32,7 @@ func TestMethodsOfResourceLabel(t *testing.T) {
require.Nil(t, err)
defer DeleteLabel(labelID)
- resourceID := "1"
+ var resourceID int64 = 1
resourceType := common.ResourceTypeRepository
// add
@@ -56,6 +56,16 @@ func TestMethodsOfResourceLabel(t *testing.T) {
require.Equal(t, 1, len(labels))
assert.Equal(t, id, r.ID)
+ // list
+ rls, err := ListResourceLabels(&models.ResourceLabelQuery{
+ LabelID: labelID,
+ ResourceType: resourceType,
+ ResourceID: resourceID,
+ })
+ require.Nil(t, err)
+ require.Equal(t, 1, len(rls))
+ assert.Equal(t, id, rls[0].ID)
+
// delete
err = DeleteResourceLabel(id)
require.Nil(t, err)
diff --git a/src/common/models/label.go b/src/common/models/label.go
index 6d949b81c..f72584832 100644
--- a/src/common/models/label.go
+++ b/src/common/models/label.go
@@ -69,7 +69,8 @@ func (l *Label) Valid(v *validation.Validation) {
type ResourceLabel struct {
ID int64 `orm:"pk;auto;column(id)"`
LabelID int64 `orm:"column(label_id)"`
- ResourceID string `orm:"column(resource_id)"`
+ ResourceID int64 `orm:"column(resource_id)"`
+ ResourceName string `orm:"column(resource_name)"`
ResourceType string `orm:"column(resource_type)"`
CreationTime time.Time `orm:"column(creation_time)"`
UpdateTime time.Time `orm:"column(update_time)"`
@@ -79,3 +80,11 @@ type ResourceLabel struct {
func (r *ResourceLabel) TableName() string {
return "harbor_resource_label"
}
+
+// ResourceLabelQuery : query parameters for the mapping relationships of resource and label
+type ResourceLabelQuery struct {
+ LabelID int64
+ ResourceID int64
+ ResourceName string
+ ResourceType string
+}
diff --git a/src/common/models/project.go b/src/common/models/project.go
index f916f3c62..5b8fd4f6a 100644
--- a/src/common/models/project.go
+++ b/src/common/models/project.go
@@ -33,7 +33,7 @@ type Project struct {
OwnerName string `orm:"-" json:"owner_name"`
Togglable bool `orm:"-" json:"togglable"`
Role int `orm:"-" json:"current_user_role_id"`
- RepoCount int `orm:"-" json:"repo_count"`
+ RepoCount int64 `orm:"-" json:"repo_count"`
Metadata map[string]string `orm:"-" json:"metadata"`
}
diff --git a/src/common/models/repo.go b/src/common/models/repo.go
index 5d5984ae7..9d1e36aeb 100644
--- a/src/common/models/repo.go
+++ b/src/common/models/repo.go
@@ -37,3 +37,12 @@ type RepoRecord struct {
func (rp *RepoRecord) TableName() string {
return RepoTable
}
+
+// RepositoryQuery : query parameters for repository
+type RepositoryQuery struct {
+ Name string
+ ProjectIDs []int64
+ ProjectName string
+ LabelID int64
+ Pagination
+}
diff --git a/src/common/utils/test/adminserver.go b/src/common/utils/test/adminserver.go
index 7a21276df..203ae05f2 100644
--- a/src/common/utils/test/adminserver.go
+++ b/src/common/utils/test/adminserver.go
@@ -75,6 +75,7 @@ var adminServerDefaultConfig = map[string]interface{}{
common.UAAVerifyCert: false,
common.UIURL: "http://myui:8888/",
common.JobServiceURL: "http://myjob:8888/",
+ common.ReadOnly: false,
}
// NewAdminserver returns a mock admin server
diff --git a/src/replication/registry/harbor_adaptor.go b/src/replication/registry/harbor_adaptor.go
index e28622f93..184689ca7 100644
--- a/src/replication/registry/harbor_adaptor.go
+++ b/src/replication/registry/harbor_adaptor.go
@@ -2,6 +2,7 @@ package registry
import (
"github.com/vmware/harbor/src/common/dao"
+ common_models "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/replication"
"github.com/vmware/harbor/src/replication/models"
@@ -30,7 +31,9 @@ func (ha *HarborAdaptor) GetNamespace(name string) models.Namespace {
//GetRepositories is used to get all the repositories under the specified namespace
func (ha *HarborAdaptor) GetRepositories(namespace string) []models.Repository {
- repos, err := dao.GetRepositoryByProjectName(namespace)
+ repos, err := dao.GetRepositories(&common_models.RepositoryQuery{
+ ProjectName: namespace,
+ })
if err != nil {
log.Errorf("failed to get repositories under namespace %s: %v", namespace, err)
return nil
diff --git a/src/ui/api/config.go b/src/ui/api/config.go
index 5fcbf6462..d3b1f1c22 100644
--- a/src/ui/api/config.go
+++ b/src/ui/api/config.go
@@ -60,6 +60,7 @@ var (
common.UAAClientSecret,
common.UAAEndpoint,
common.UAAVerifyCert,
+ common.ReadOnly,
}
stringKeys = []string{
@@ -97,6 +98,7 @@ var (
common.SelfRegistration,
common.LDAPVerifyCert,
common.UAAVerifyCert,
+ common.ReadOnly,
}
passwordKeys = []string{
diff --git a/src/ui/api/project.go b/src/ui/api/project.go
index 8544ca14c..a292ca9fe 100644
--- a/src/ui/api/project.go
+++ b/src/ui/api/project.go
@@ -199,6 +199,8 @@ func (p *ProjectAPI) Get() {
}
}
+ p.populateProperties(p.project)
+
p.Data["json"] = p.project
p.ServeJSON()
}
@@ -268,7 +270,9 @@ func (p *ProjectAPI) Deletable() {
}
func deletable(projectID int64) (*deletableResp, error) {
- count, err := dao.GetTotalOfRepositoriesByProject([]int64{projectID}, "")
+ count, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
+ ProjectIDs: []int64{projectID},
+ })
if err != nil {
return nil, err
}
@@ -372,25 +376,7 @@ func (p *ProjectAPI) List() {
}
for _, project := range result.Projects {
- if p.SecurityCtx.IsAuthenticated() {
- roles := p.SecurityCtx.GetProjectRoles(project.ProjectID)
- if len(roles) != 0 {
- project.Role = roles[0]
- }
-
- if project.Role == common.RoleProjectAdmin ||
- p.SecurityCtx.IsSysAdmin() {
- project.Togglable = true
- }
- }
-
- repos, err := dao.GetRepositoryByProjectName(project.Name)
- if err != nil {
- log.Errorf("failed to get repositories of project %s: %v", project.Name, err)
- p.CustomAbort(http.StatusInternalServerError, "")
- }
-
- project.RepoCount = len(repos)
+ p.populateProperties(project)
}
p.SetPaginationHeader(result.Total, page, size)
@@ -398,6 +384,30 @@ func (p *ProjectAPI) List() {
p.ServeJSON()
}
+func (p *ProjectAPI) populateProperties(project *models.Project) {
+ if p.SecurityCtx.IsAuthenticated() {
+ roles := p.SecurityCtx.GetProjectRoles(project.ProjectID)
+ if len(roles) != 0 {
+ project.Role = roles[0]
+ }
+
+ if project.Role == common.RoleProjectAdmin ||
+ p.SecurityCtx.IsSysAdmin() {
+ project.Togglable = true
+ }
+ }
+
+ total, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
+ ProjectIDs: []int64{project.ProjectID},
+ })
+ if err != nil {
+ log.Errorf("failed to get total of repositories of project %d: %v", project.ProjectID, err)
+ p.CustomAbort(http.StatusInternalServerError, "")
+ }
+
+ project.RepoCount = total
+}
+
// Put ...
func (p *ProjectAPI) Put() {
if !p.SecurityCtx.IsAuthenticated() {
diff --git a/src/ui/api/repository.go b/src/ui/api/repository.go
index b57786ff6..cff3f9444 100644
--- a/src/ui/api/repository.go
+++ b/src/ui/api/repository.go
@@ -96,6 +96,12 @@ func (ra *RepositoryAPI) Get() {
return
}
+ labelID, err := ra.GetInt64("label_id", 0)
+ if err != nil {
+ ra.HandleBadRequest(fmt.Sprintf("invalid label_id: %s", ra.GetString("label_id")))
+ return
+ }
+
exist, err := ra.ProjectMgr.Exists(projectID)
if err != nil {
ra.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %d",
@@ -117,33 +123,33 @@ func (ra *RepositoryAPI) Get() {
return
}
- keyword := ra.GetString("q")
+ query := &models.RepositoryQuery{
+ ProjectIDs: []int64{projectID},
+ Name: ra.GetString("q"),
+ LabelID: labelID,
+ }
+ query.Page, query.Size = ra.GetPaginationParams()
- total, err := dao.GetTotalOfRepositoriesByProject(
- []int64{projectID}, keyword)
+ total, err := dao.GetTotalOfRepositories(query)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get total of repositories of project %d: %v",
projectID, err))
return
}
- page, pageSize := ra.GetPaginationParams()
-
- repositories, err := getRepositories(projectID,
- keyword, pageSize, pageSize*(page-1))
+ repositories, err := getRepositories(query)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get repository: %v", err))
return
}
- ra.SetPaginationHeader(total, page, pageSize)
+ ra.SetPaginationHeader(total, query.Page, query.Size)
ra.Data["json"] = repositories
ra.ServeJSON()
}
-func getRepositories(projectID int64, keyword string,
- limit, offset int64) ([]*repoResp, error) {
- repositories, err := dao.GetRepositoriesByProject(projectID, keyword, limit, offset)
+func getRepositories(query *models.RepositoryQuery) ([]*repoResp, error) {
+ repositories, err := dao.GetRepositories(query)
if err != nil {
return nil, err
}
@@ -171,8 +177,7 @@ func assembleRepos(repositories []*models.RepoRecord) ([]*repoResp, error) {
}
repo.TagsCount = int64(len(tags))
- labels, err := dao.GetLabelsOfResource(common.ResourceTypeRepository,
- strconv.FormatInt(repository.RepositoryID, 10))
+ labels, err := dao.GetLabelsOfResource(common.ResourceTypeRepository, repository.RepositoryID)
if err != nil {
log.Errorf("failed to get labels of repository %s: %v", repository.Name, err)
} else {
@@ -385,6 +390,11 @@ func (ra *RepositoryAPI) GetTag() {
// GetTags returns tags of a repository
func (ra *RepositoryAPI) GetTags() {
repoName := ra.GetString(":splat")
+ labelID, err := ra.GetInt64("label_id", 0)
+ if err != nil {
+ ra.HandleBadRequest(fmt.Sprintf("invalid label_id: %s", ra.GetString("label_id")))
+ return
+ }
projectName, _ := utils.ParseRepository(repoName)
exist, err := ra.ProjectMgr.Exists(projectName)
@@ -420,7 +430,31 @@ func (ra *RepositoryAPI) GetTags() {
return
}
- ra.Data["json"] = assembleTags(client, repoName, tags, ra.SecurityCtx.GetUsername())
+ // filter tags by label ID
+ if labelID > 0 {
+ rls, err := dao.ListResourceLabels(&models.ResourceLabelQuery{
+ LabelID: labelID,
+ ResourceType: common.ResourceTypeImage,
+ })
+ if err != nil {
+ ra.HandleInternalServerError(fmt.Sprintf("failed to list resource labels: %v", err))
+ return
+ }
+ labeledTags := map[string]struct{}{}
+ for _, rl := range rls {
+ labeledTags[strings.Split(rl.ResourceName, ":")[1]] = struct{}{}
+ }
+ ts := []string{}
+ for _, tag := range tags {
+ if _, ok := labeledTags[tag]; ok {
+ ts = append(ts, tag)
+ }
+ }
+ tags = ts
+ }
+
+ ra.Data["json"] = assembleTags(client, repoName, tags,
+ ra.SecurityCtx.GetUsername())
ra.ServeJSON()
}
@@ -443,6 +477,15 @@ func assembleTags(client *registry.Repository, repository string,
for _, t := range tags {
item := &tagResp{}
+ // 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
+ }
+
// the detail information of tag
tagDetail, err := getTagDetail(client, t)
if err != nil {
@@ -468,15 +511,6 @@ func assembleTags(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)
}
diff --git a/src/ui/api/repository_label.go b/src/ui/api/repository_label.go
index 95e92cdfc..756c78d9d 100644
--- a/src/ui/api/repository_label.go
+++ b/src/ui/api/repository_label.go
@@ -144,24 +144,20 @@ func (r *RepositoryLabelAPI) AddToImage() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeImage,
- ResourceID: fmt.Sprintf("%s:%s", r.repository.Name, r.tag),
+ ResourceName: 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)
+ r.removeLabel(common.ResourceTypeImage,
+ fmt.Sprintf("%s:%s", r.repository.Name, r.tag), r.label.ID)
}
// GetOfRepository returns labels of a repository
func (r *RepositoryLabelAPI) GetOfRepository() {
- r.getLabels(common.ResourceTypeRepository, strconv.FormatInt(r.repository.RepositoryID, 10))
+ r.getLabels(common.ResourceTypeRepository, r.repository.RepositoryID)
}
// AddToRepository adds the label to a repository
@@ -169,26 +165,21 @@ func (r *RepositoryLabelAPI) AddToRepository() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeRepository,
- ResourceID: strconv.FormatInt(r.repository.RepositoryID, 10),
+ ResourceID: r.repository.RepositoryID,
}
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)
+ r.removeLabel(common.ResourceTypeRepository, r.repository.RepositoryID, r.label.ID)
}
-func (r *RepositoryLabelAPI) getLabels(rType, rID string) {
- labels, err := dao.GetLabelsOfResource(rType, rID)
+func (r *RepositoryLabelAPI) getLabels(rType string, rIDOrName interface{}) {
+ labels, err := dao.GetLabelsOfResource(rType, rIDOrName)
if err != nil {
- r.HandleInternalServerError(fmt.Sprintf("failed to get labels of resource %s %s: %v",
- rType, rID, err))
+ r.HandleInternalServerError(fmt.Sprintf("failed to get labels of resource %s %v: %v",
+ rType, rIDOrName, err))
return
}
r.Data["json"] = labels
@@ -196,10 +187,16 @@ func (r *RepositoryLabelAPI) getLabels(rType, rID string) {
}
func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) {
- rlabel, err := dao.GetResourceLabel(rl.ResourceType, rl.ResourceID, rl.LabelID)
+ var rIDOrName interface{}
+ if rl.ResourceID != 0 {
+ rIDOrName = rl.ResourceID
+ } else {
+ rIDOrName = rl.ResourceName
+ }
+ rlabel, err := dao.GetResourceLabel(rl.ResourceType, rIDOrName, 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))
+ r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %v: %v",
+ rl.LabelID, rl.ResourceType, rIDOrName, err))
return
}
@@ -208,8 +205,8 @@ func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) {
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))
+ r.HandleInternalServerError(fmt.Sprintf("failed to add label %d to resource %s %v: %v",
+ rl.LabelID, rl.ResourceType, rIDOrName, err))
return
}
@@ -217,22 +214,22 @@ func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) {
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)
+func (r *RepositoryLabelAPI) removeLabel(rType string, rIDOrName interface{}, labelID int64) {
+ rl, err := dao.GetResourceLabel(rType, rIDOrName, 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))
+ r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %v: %v",
+ labelID, rType, rIDOrName, err))
return
}
- if rlabel == nil {
+ if rl == nil {
r.HandleNotFound(fmt.Sprintf("label %d of resource %s %s not found",
- rl.LabelID, rl.ResourceType, rl.ResourceID))
+ labelID, rType, rIDOrName))
return
}
- if err = dao.DeleteResourceLabel(rlabel.ID); err != nil {
+ if err = dao.DeleteResourceLabel(rl.ID); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete resource label record %d: %v",
- rlabel.ID, err))
+ rl.ID, err))
return
}
}
diff --git a/src/ui/api/repository_test.go b/src/ui/api/repository_test.go
index 45d11db97..c0efc2c6c 100644
--- a/src/ui/api/repository_test.go
+++ b/src/ui/api/repository_test.go
@@ -27,7 +27,7 @@ func TestGetRepos(t *testing.T) {
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := "1"
- keyword := "hello-world"
+ keyword := "library/hello-world"
fmt.Println("Testing Repos Get API")
//-------------------case 1 : response code = 200------------------------//
diff --git a/src/ui/api/search.go b/src/ui/api/search.go
index ded814f1c..468e481aa 100644
--- a/src/ui/api/search.go
+++ b/src/ui/api/search.go
@@ -99,13 +99,15 @@ func (s *SearchAPI) Get() {
}
}
- repos, err := dao.GetRepositoryByProjectName(p.Name)
+ total, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
+ ProjectIDs: []int64{p.ProjectID},
+ })
if err != nil {
- log.Errorf("failed to get repositories of project %s: %v", p.Name, err)
+ log.Errorf("failed to get total of repositories of project %d: %v", p.ProjectID, err)
s.CustomAbort(http.StatusInternalServerError, "")
}
- p.RepoCount = len(repos)
+ p.RepoCount = total
projectResult = append(projectResult, p)
}
@@ -124,7 +126,7 @@ func (s *SearchAPI) Get() {
func filterRepositories(projects []*models.Project, keyword string) (
[]map[string]interface{}, error) {
- repositories, err := dao.GetAllRepositories()
+ repositories, err := dao.GetRepositories()
if err != nil {
return nil, err
}
diff --git a/src/ui/api/statistic.go b/src/ui/api/statistic.go
index f2ab3283e..71d4ce77e 100644
--- a/src/ui/api/statistic.go
+++ b/src/ui/api/statistic.go
@@ -64,17 +64,22 @@ func (s *StatisticAPI) Get() {
}
statistic[PubPC] = (int64)(len(pubProjs))
-
- ids := []int64{}
- for _, p := range pubProjs {
- ids = append(ids, p.ProjectID)
+ if len(pubProjs) == 0 {
+ statistic[PubRC] = 0
+ } else {
+ ids := []int64{}
+ for _, p := range pubProjs {
+ ids = append(ids, p.ProjectID)
+ }
+ n, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
+ ProjectIDs: ids,
+ })
+ if err != nil {
+ log.Errorf("failed to get total of public repositories: %v", err)
+ s.CustomAbort(http.StatusInternalServerError, "")
+ }
+ statistic[PubRC] = n
}
- n, err := dao.GetTotalOfRepositoriesByProject(ids, "")
- if err != nil {
- log.Errorf("failed to get total of public repositories: %v", err)
- s.CustomAbort(http.StatusInternalServerError, "")
- }
- statistic[PubRC] = n
if s.SecurityCtx.IsSysAdmin() {
result, err := s.ProjectMgr.List(nil)
@@ -85,7 +90,7 @@ func (s *StatisticAPI) Get() {
statistic[TPC] = result.Total
statistic[PriPC] = result.Total - statistic[PubPC]
- n, err := dao.GetTotalOfRepositories("")
+ n, err := dao.GetTotalOfRepositories()
if err != nil {
log.Errorf("failed to get total of repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
@@ -107,20 +112,25 @@ func (s *StatisticAPI) Get() {
}
statistic[PriPC] = result.Total
+ if result.Total == 0 {
+ statistic[PriRC] = 0
+ } else {
+ ids := []int64{}
+ for _, p := range result.Projects {
+ ids = append(ids, p.ProjectID)
+ }
- ids := []int64{}
- for _, p := range result.Projects {
- ids = append(ids, p.ProjectID)
+ n, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
+ ProjectIDs: ids,
+ })
+ if err != nil {
+ s.HandleInternalServerError(fmt.Sprintf(
+ "failed to get total of repositories for user %s: %v",
+ s.username, err))
+ return
+ }
+ statistic[PriRC] = n
}
-
- n, err = dao.GetTotalOfRepositoriesByProject(ids, "")
- if err != nil {
- s.HandleInternalServerError(fmt.Sprintf(
- "failed to get total of repositories for user %s: %v",
- s.username, err))
- return
- }
- statistic[PriRC] = n
}
s.Data["json"] = statistic
diff --git a/src/ui/api/systeminfo.go b/src/ui/api/systeminfo.go
index 4974ffdbd..699d2fef6 100644
--- a/src/ui/api/systeminfo.go
+++ b/src/ui/api/systeminfo.go
@@ -37,7 +37,7 @@ type SystemInfoAPI struct {
}
const defaultRootCert = "/etc/ui/ca/ca.crt"
-const harborVersionFile = "/harbor/VERSION"
+const harborVersionFile = "/harbor/UIVERSION"
//SystemInfo models for system info.
type SystemInfo struct {
diff --git a/src/ui/api/utils.go b/src/ui/api/utils.go
index 0f1c2acac..26cae3564 100644
--- a/src/ui/api/utils.go
+++ b/src/ui/api/utils.go
@@ -86,7 +86,7 @@ func SyncRegistry(pm promgr.ProjectManager) error {
}
var repoRecordsInDB []*models.RepoRecord
- repoRecordsInDB, err = dao.GetAllRepositories()
+ repoRecordsInDB, err = dao.GetRepositories()
if err != nil {
log.Errorf("error occurred while getting all registories. %v", err)
return err
diff --git a/src/ui/config/config.go b/src/ui/config/config.go
index 2967e9222..8bdfcf402 100644
--- a/src/ui/config/config.go
+++ b/src/ui/config/config.go
@@ -495,3 +495,13 @@ func UAASettings() (*models.UAASettings, error) {
}
return us, nil
}
+
+// ReadOnly returns a bool to indicates if Harbor is in read only mode.
+func ReadOnly() bool {
+ cfg, err := mg.Get()
+ if err != nil {
+ log.Errorf("Failed to get configuration, will return false as read only, error: %v", err)
+ return false
+ }
+ return cfg[common.ReadOnly].(bool)
+}
diff --git a/src/ui/config/config_test.go b/src/ui/config/config_test.go
index 0b0698b65..016d19769 100644
--- a/src/ui/config/config_test.go
+++ b/src/ui/config/config_test.go
@@ -146,6 +146,9 @@ func TestConfig(t *testing.T) {
if !WithAdmiral() {
t.Errorf("WithAdmiral should be true")
}
+ if ReadOnly() {
+ t.Errorf("ReadOnly should be false")
+ }
if AdmiralEndpoint() != "http://www.vmware.com" {
t.Errorf("Unexpected admiral endpoint: %s", AdmiralEndpoint())
}
diff --git a/src/ui/filter/readonly.go b/src/ui/filter/readonly.go
new file mode 100644
index 000000000..44dbb8982
--- /dev/null
+++ b/src/ui/filter/readonly.go
@@ -0,0 +1,76 @@
+// 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 filter
+
+import (
+ "net/http"
+ "regexp"
+
+ "github.com/astaxie/beego/context"
+ "github.com/vmware/harbor/src/ui/config"
+)
+
+const (
+ repoURL = `^/api/repositories/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)(?:[a-z0-9]+(?:[._-][a-z0-9]+)*)$`
+ tagURL = `^/api/repositories/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)tags/([\w][\w.-]{0,127})$`
+ labelURL = `^/api/repositories/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)tags/([\w][\w.-]{0,127})/labels/[0-9]+$`
+)
+
+//ReadonlyFilter filters the delete repo/tag request and returns 503.
+func ReadonlyFilter(ctx *context.Context) {
+ filter(ctx.Request, ctx.ResponseWriter)
+}
+
+func filter(req *http.Request, resp http.ResponseWriter) {
+ if !config.ReadOnly() {
+ return
+ }
+ if req.Method != http.MethodDelete {
+ return
+ }
+ if matchRepoTagDelete(req) {
+ resp.WriteHeader(http.StatusServiceUnavailable)
+ }
+}
+
+// Only block repository and tag deletion
+func matchRepoTagDelete(req *http.Request) bool {
+ if inWhiteList(req) {
+ return false
+ }
+
+ re := regexp.MustCompile(tagURL)
+ s := re.FindStringSubmatch(req.URL.Path)
+ if len(s) == 3 {
+ return true
+ }
+
+ re = regexp.MustCompile(repoURL)
+ s = re.FindStringSubmatch(req.URL.Path)
+ if len(s) == 2 {
+ return true
+ }
+
+ return false
+}
+
+func inWhiteList(req *http.Request) bool {
+ re := regexp.MustCompile(labelURL)
+ s := re.FindStringSubmatch(req.URL.Path)
+ if len(s) == 3 {
+ return true
+ }
+ return false
+}
diff --git a/src/ui/filter/readonly_test.go b/src/ui/filter/readonly_test.go
new file mode 100644
index 000000000..ede66f885
--- /dev/null
+++ b/src/ui/filter/readonly_test.go
@@ -0,0 +1,83 @@
+// 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 filter
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/vmware/harbor/src/common"
+ utilstest "github.com/vmware/harbor/src/common/utils/test"
+ "github.com/vmware/harbor/src/ui/config"
+)
+
+func TestReadonlyFilter(t *testing.T) {
+
+ var defaultConfig = map[string]interface{}{
+ common.ExtEndpoint: "host01.com",
+ common.AUTHMode: "db_auth",
+ common.CfgExpiration: 5,
+ common.TokenExpiration: 30,
+ common.DatabaseType: "mysql",
+ common.MySQLHost: "127.0.0.1",
+ common.MySQLPort: 3306,
+ common.MySQLUsername: "root",
+ common.MySQLPassword: "root123",
+ common.MySQLDatabase: "registry",
+ common.SQLiteFile: "/tmp/registry.db",
+ common.ReadOnly: true,
+ }
+ adminServer, err := utilstest.NewAdminserver(defaultConfig)
+ if err != nil {
+ panic(err)
+ }
+ defer adminServer.Close()
+ if err := os.Setenv("ADMINSERVER_URL", adminServer.URL); err != nil {
+ panic(err)
+ }
+ if err := config.Init(); err != nil {
+ panic(err)
+ }
+
+ assert := assert.New(t)
+ req1, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/ubuntu", nil)
+ rec := httptest.NewRecorder()
+ filter(req1, rec)
+ assert.Equal(http.StatusServiceUnavailable, rec.Code)
+
+ req2, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/hello-world", nil)
+ rec = httptest.NewRecorder()
+ filter(req2, rec)
+ assert.Equal(http.StatusServiceUnavailable, rec.Code)
+
+ req3, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/hello-world/tags/14.04", nil)
+ rec = httptest.NewRecorder()
+ filter(req3, rec)
+ assert.Equal(http.StatusServiceUnavailable, rec.Code)
+
+ req4, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/hello-world/tags/latest", nil)
+ rec = httptest.NewRecorder()
+ filter(req4, rec)
+ assert.Equal(http.StatusServiceUnavailable, rec.Code)
+
+ req5, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/vmware/hello-world", nil)
+ rec = httptest.NewRecorder()
+ filter(req5, rec)
+ assert.Equal(http.StatusServiceUnavailable, rec.Code)
+
+}
diff --git a/src/ui/main.go b/src/ui/main.go
index 4a8d8bf2a..c80f7d81e 100644
--- a/src/ui/main.go
+++ b/src/ui/main.go
@@ -143,6 +143,7 @@ func main() {
filter.Init()
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
+ beego.InsertFilter("/*", beego.BeforeRouter, filter.ReadonlyFilter)
beego.InsertFilter("/api/*", beego.BeforeRouter, filter.MediaTypeFilter("application/json"))
initRouters()
diff --git a/src/ui/proxy/interceptors.go b/src/ui/proxy/interceptors.go
index 3284815a9..b18d1c155 100644
--- a/src/ui/proxy/interceptors.go
+++ b/src/ui/proxy/interceptors.go
@@ -154,6 +154,20 @@ func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
uh.next.ServeHTTP(rw, req)
}
+type readonlyHandler struct {
+ next http.Handler
+}
+
+func (rh readonlyHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ if config.ReadOnly() {
+ if req.Method == http.MethodDelete || req.Method == http.MethodPost || req.Method == http.MethodPatch {
+ http.Error(rw, "Upload/Delete is prohibited in read only mode.", http.StatusServiceUnavailable)
+ return
+ }
+ }
+ rh.next.ServeHTTP(rw, req)
+}
+
type listReposHandler struct {
next http.Handler
}
diff --git a/src/ui/proxy/proxy.go b/src/ui/proxy/proxy.go
index 9fed58932..46ab3266a 100644
--- a/src/ui/proxy/proxy.go
+++ b/src/ui/proxy/proxy.go
@@ -41,7 +41,7 @@ func Init(urls ...string) error {
return err
}
Proxy = httputil.NewSingleHostReverseProxy(targetURL)
- handlers = handlerChain{head: urlHandler{next: listReposHandler{next: contentTrustHandler{next: vulnerableHandler{next: Proxy}}}}}
+ handlers = handlerChain{head: readonlyHandler{next: urlHandler{next: listReposHandler{next: contentTrustHandler{next: vulnerableHandler{next: Proxy}}}}}}
return nil
}
diff --git a/src/ui/utils/utils.go b/src/ui/utils/utils.go
index 6118551c5..503d2b2f7 100644
--- a/src/ui/utils/utils.go
+++ b/src/ui/utils/utils.go
@@ -33,7 +33,7 @@ import (
// ScanAllImages scans all images of Harbor by submiting jobs to jobservice, the whole process will move on if failed to submit any job of a single image.
func ScanAllImages() error {
- repos, err := dao.GetAllRepositories()
+ repos, err := dao.GetRepositories()
if err != nil {
log.Errorf("Failed to list all repositories, error: %v", err)
return err
@@ -46,7 +46,9 @@ func ScanAllImages() error {
// ScanImagesByProjectID scans all images under a projet, the whole process will move on if failed to submit any job of a single image.
func ScanImagesByProjectID(id int64) error {
- repos, err := dao.GetRepositoriesByProject(id, "", 0, 0)
+ repos, err := dao.GetRepositories(&models.RepositoryQuery{
+ ProjectIDs: []int64{id},
+ })
if err != nil {
log.Errorf("Failed list repositories in project %d, error: %v", id, err)
return err
diff --git a/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.css.ts b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.css.ts
new file mode 100644
index 000000000..4653a70d1
--- /dev/null
+++ b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.css.ts
@@ -0,0 +1,23 @@
+export const CREATE_EDIT_LABEL_STYLE: string = `
+ .form-group-label-override {
+ font-size: 14px;
+ font-weight: 400;
+ }
+
+ form{margin-bottom:-10px;padding-top:0; margin-top: 20px;width: 100%;background-color: #eee; border:1px solid #ccc;}
+ form .form-group{display:inline-flex;padding-left: 70px;}
+ form .form-group>label:first-child{width: auto;}
+ section{padding:.5rem 0;}
+ section> label{margin-left: 20px;}
+
+ .dropdown-menu{display:inline-block;width:166px; padding:6px;}
+ .dropdown-item{ display:inline-flex; margin:2px 4px;
+ display: inline-block;padding: 0px; width:30px;height:24px; text-align: center;line-height: 24px;}
+ .btnColor{
+ margin: 0 !important;
+ padding: 0;
+ width: 26px;
+ height:22px;
+ min-width: 26px;}
+ .dropdown-item{border:0px; color: white; font-size:12px;}
+`;
\ No newline at end of file
diff --git a/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.html.ts b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.html.ts
new file mode 100644
index 000000000..6c12fee4d
--- /dev/null
+++ b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.html.ts
@@ -0,0 +1,36 @@
+export const CREATE_EDIT_LABEL_TEMPLATE: string = `
+
+
+
`;
\ No newline at end of file
diff --git a/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.spec.ts b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.spec.ts
new file mode 100644
index 000000000..fc4f7ccda
--- /dev/null
+++ b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.spec.ts
@@ -0,0 +1,85 @@
+
+import { ComponentFixture, TestBed, async } from '@angular/core/testing';
+import { NoopAnimationsModule } from "@angular/platform-browser/animations";
+import { SharedModule } from '../shared/shared.module';
+
+import { FilterComponent } from '../filter/filter.component';
+
+import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
+import { ErrorHandler } from '../error-handler/error-handler';
+import {Label} from '../service/interface';
+import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
+import {CreateEditLabelComponent} from "./create-edit-label.component";
+import {LabelDefaultService, LabelService} from "../service/label.service";
+
+describe('CreateEditLabelComponent (inline template)', () => {
+
+ let mockOneData: Label = {
+ color: "#9b0d54",
+ creation_time: "",
+ description: "",
+ id: 1,
+ name: "label0-g",
+ project_id: 0,
+ scope: "g",
+ update_time: "",
+ }
+
+ let comp: CreateEditLabelComponent;
+ let fixture: ComponentFixture;
+
+ let config: IServiceConfig = {
+ systemInfoEndpoint: '/api/label/testing'
+ };
+
+ let labelService: LabelService;
+
+ let spy: jasmine.Spy;
+ let spyOne: jasmine.Spy;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ SharedModule,
+ NoopAnimationsModule
+ ],
+ declarations: [
+ FilterComponent,
+ CreateEditLabelComponent,
+ InlineAlertComponent ],
+ providers: [
+ ErrorHandler,
+ { provide: SERVICE_CONFIG, useValue: config },
+ { provide: LabelService, useClass: LabelDefaultService }
+ ]
+ });
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CreateEditLabelComponent);
+ comp = fixture.componentInstance;
+
+ labelService = fixture.debugElement.injector.get(LabelService);
+
+ spy = spyOn(labelService, 'getLabels').and.returnValue(Promise.resolve(mockOneData));
+ spyOne = spyOn(labelService, 'createLabel').and.returnValue(Promise.resolve(mockOneData));
+
+ fixture.detectChanges();
+
+ comp.openModal();
+ fixture.detectChanges();
+ });
+
+ it('should be created', () => {
+ fixture.detectChanges();
+ expect(comp).toBeTruthy();
+ });
+
+ it('should get label and open modal', () => {
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ fixture.detectChanges();
+ expect(comp.labelModel.name).toEqual('');
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.ts b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.ts
new file mode 100644
index 000000000..c1e838231
--- /dev/null
+++ b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.ts
@@ -0,0 +1,164 @@
+// 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.
+import {
+ Component,
+ Output,
+ EventEmitter,
+ OnDestroy,
+ Input, OnInit, ViewChild
+} from '@angular/core';
+
+
+import {Label} from '../service/interface';
+
+import { CREATE_EDIT_LABEL_STYLE } from './create-edit-label.component.css';
+import { CREATE_EDIT_LABEL_TEMPLATE } from './create-edit-label.component.html';
+
+import {toPromise, clone, compareValue} from '../utils';
+
+import {Subject} from "rxjs/Subject";
+
+import {LabelService} from "../service/label.service";
+import {ErrorHandler} from "../error-handler/error-handler";
+import {NgForm} from "@angular/forms";
+
+@Component({
+ selector: 'hbr-create-edit-label',
+ template: CREATE_EDIT_LABEL_TEMPLATE,
+ styles: [CREATE_EDIT_LABEL_STYLE]
+})
+
+export class CreateEditLabelComponent implements OnInit, OnDestroy {
+ formShow: boolean;
+ inProgress: boolean;
+ copeLabelModel: Label;
+ labelModel: Label = this.initLabel();
+ labelId = 0;
+
+ nameChecker: Subject = new Subject();
+ checkOnGoing: boolean;
+ isLabelNameExist = false;
+
+ labelColor = ['#00ab9a', '#9da3db', '#be90d6', '#9b0d54', '#f52f22', '#747474', '#0095d3', '#f38b00', ' #62a420', '#89cbdf', '#004a70', '#9460b8'];
+
+ labelForm: NgForm;
+ @ViewChild('labelForm')
+ currentForm: NgForm;
+
+ @Input() projectId: number;
+ @Input() scope: string;
+ @Output() reload = new EventEmitter();
+
+ constructor(
+ private labelService: LabelService,
+ private errorHandler: ErrorHandler,
+ ) { }
+
+ ngOnInit(): void {
+ this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((name: string) => {
+ this.checkOnGoing = true;
+ toPromise