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(this.labelService.getLabels(this.scope, this.projectId)) + .then(targets => { + if (targets && targets.length) { + if (targets.find(m => m.name === name)) { + this.isLabelNameExist = true; + } else { + this.isLabelNameExist = false; + }; + }else { + this.isLabelNameExist = false; + } + this.checkOnGoing = false; + }).catch(error => { + this.checkOnGoing = false; + this.errorHandler.error(error) + }); + }); + } + + ngOnDestroy(): void { + this.nameChecker.unsubscribe(); + } + + initLabel(): Label { + return { + name: '', + description: '', + color: '', + scope: '', + project_id: 0 + }; + } + openModal(): void { + this.labelModel = this.initLabel(); + this.formShow = true; + this.labelId = 0; + this.copeLabelModel = null; + } + + editModel(labelId: number, label: Label[]): void { + this.labelModel = clone(label[0]); + this.formShow = true; + this.labelId = labelId; + this.copeLabelModel = clone(label[0]); + } + + public get hasChanged(): boolean { + return !compareValue(this.copeLabelModel, this.labelModel); + } + + public get isValid(): boolean { + return !(this.checkOnGoing || this.isLabelNameExist || !(this.currentForm && this.currentForm.valid) || !this.hasChanged || this.inProgress); + } + + existValid(text: string): void { + if (text) { + this.nameChecker.next(text); + } + } + + onSubmit(): void { + this.inProgress = true; + if (this.labelId <= 0) { + this.labelModel.scope = this.scope; + this.labelModel.project_id = this.projectId; + toPromise