From 086ebbfe84f97e8776088f169a47392045ae5adc Mon Sep 17 00:00:00 2001 From: Cyril Jouve Date: Mon, 14 May 2018 12:21:51 +0200 Subject: [PATCH 01/32] remove unused upstream --- make/common/templates/nginx/nginx.http.conf | 5 ----- make/common/templates/nginx/nginx.https.conf | 4 ---- 2 files changed, 9 deletions(-) diff --git a/make/common/templates/nginx/nginx.http.conf b/make/common/templates/nginx/nginx.http.conf index 9e4123a9d..5fd84ac5a 100644 --- a/make/common/templates/nginx/nginx.http.conf +++ b/make/common/templates/nginx/nginx.http.conf @@ -12,11 +12,6 @@ http { # this is necessary for us to be able to disable request buffering in all cases proxy_http_version 1.1; - - upstream registry { - server registry:5000; - } - upstream ui { server ui:8080; } diff --git a/make/common/templates/nginx/nginx.https.conf b/make/common/templates/nginx/nginx.https.conf index d0b295b7a..faf0ba842 100644 --- a/make/common/templates/nginx/nginx.https.conf +++ b/make/common/templates/nginx/nginx.https.conf @@ -13,10 +13,6 @@ http { # this is necessary for us to be able to disable request buffering in all cases proxy_http_version 1.1; - upstream registry { - server registry:5000; - } - upstream ui { server ui:8080; } From bf0ba46c46653580cd8e771cfa905133bdead204 Mon Sep 17 00:00:00 2001 From: Yuki Date: Mon, 25 Jun 2018 09:40:14 +0800 Subject: [PATCH 02/32] Modify installation_guide.md Modify HTTS of network port 4443 to HTTPS. --- docs/installation_guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation_guide.md b/docs/installation_guide.md index ca44e9a73..8939b5363 100644 --- a/docs/installation_guide.md +++ b/docs/installation_guide.md @@ -34,7 +34,7 @@ Harbor is deployed as several Docker containers, and, therefore, can be deployed |Port|Protocol|Description| |---|---|---| |443|HTTPS|Harbor UI and API will accept requests on this port for https protocol| -|4443|HTTS|Connections to the Docker Content Trust service for Harbor, only needed when Notary is enabled| +|4443|HTTPS|Connections to the Docker Content Trust service for Harbor, only needed when Notary is enabled| |80|HTTP|Harbor UI and API will accept requests on this port for http protocol| ## Installation Steps From ee60eaec163820c97b0b59cbb20259b69c902932 Mon Sep 17 00:00:00 2001 From: mricher Date: Wed, 27 Jun 2018 16:50:26 +0200 Subject: [PATCH 03/32] Change empty_subj to fix openssl issue --- make/prepare | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make/prepare b/make/prepare index 4896f8798..e78d2afa0 100755 --- a/make/prepare +++ b/make/prepare @@ -504,7 +504,7 @@ def openssl_installed(): if customize_crt == 'on' and openssl_installed(): shell_stat = subprocess.check_call(["which", "openssl"], stdout=FNULL, stderr=subprocess.STDOUT) - empty_subj = "/C=/ST=/L=/O=/CN=/" + empty_subj = "/" private_key_pem = os.path.join(config_dir, "ui", "private_key.pem") root_crt = os.path.join(config_dir, "registry", "root.crt") create_root_cert(empty_subj, key_path=private_key_pem, cert_path=root_crt) From 2baee7a552fae15e1ef0d32fc866b98557f4e6b7 Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Fri, 29 Jun 2018 17:32:08 +0800 Subject: [PATCH 04/32] Update the swagger file Fix #5168 by correcting the types of properties used in ProjectMetadata --- docs/swagger.yaml | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 66ed3fe2b..ba1d9c0b8 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2720,25 +2720,24 @@ definitions: type: object properties: public: - type: integer - format: int - description: The public status of the project. + type: string + description: The public status of the project. The valid values are "true", "false". enable_content_trust: - type: boolean + type: string description: >- Whether content trust is enabled or not. If it is enabled, user cann't - pull unsigned images from this project. + pull unsigned images from this project. The valid values are "true", "false". prevent_vulnerable_images_from_running: - type: boolean - description: Whether prevent the vulnerable images from running. + type: string + description: Whether prevent the vulnerable images from running. The valid values are "true", "false". prevent_vulnerable_images_from_running_severity: type: string description: >- If the vulnerability is high than severity defined here, the images - cann't be pulled. + cann't be pulled. The valid values are "negligible", "low", "medium", "high", "critical". automatically_scan_images_on_push: - type: boolean - description: Whether scan images automatically when pushing. + type: string + description: Whether scan images automatically when pushing. The valid values are "true", "false". Manifest: type: object properties: @@ -2989,9 +2988,7 @@ definitions: The replication policy filter kind. The valid values are project, repository and tag. value: - type: - - string - - integer + type: string description: The value of replication policy filter. When creating repository and tag filter, filling it with the pattern as string. When creating label filter, filling it with label ID as integer. pattern: type: string From edbe2fe620eaafa0b91905ac7f46caa100998ce6 Mon Sep 17 00:00:00 2001 From: "Deng, Qian" Date: Mon, 2 Jul 2018 21:23:47 +0800 Subject: [PATCH 05/32] Update migrator to 1 6 0 1. Add new alembic_pg folder for postgres 2. Add migration file for 1.6.0 3. Update version to 1.6.0 4. update migrator dockerfile --- Makefile | 2 +- .../postgresql/0001_initial_schema.up.sql | 2 +- src/common/dao/version.go | 2 +- src/ui_ng/lib/package.json | 2 +- src/ui_ng/package.json | 2 +- tools/migration/Dockerfile | 2 +- tools/migration/cfg/run.py | 2 +- .../db/alembic/{ => mysql}/alembic.sql | 0 .../db/alembic/{ => mysql}/alembic.tpl | 2 +- .../db/alembic/{ => mysql}/db_meta.py | 2 +- .../{ => mysql}/migration_harbor/env.py | 0 .../migration_harbor/script.py.mako | 0 .../migration_harbor/versions/0_1_1.py | 0 .../migration_harbor/versions/0_3_0.py | 0 .../migration_harbor/versions/0_4_0.py | 0 .../migration_harbor/versions/1_2_0.py | 0 .../migration_harbor/versions/1_3_0.py | 0 .../migration_harbor/versions/1_4_0.py | 0 .../migration_harbor/versions/1_5_0.py | 0 .../migration/db/alembic/postgres/alembic.tpl | 75 +++++ .../migration/db/alembic/postgres/db_meta.py | 284 ++++++++++++++++++ .../alembic/postgres/migration_harbor/README | 1 + .../alembic/postgres/migration_harbor/env.py | 70 +++++ .../postgres/migration_harbor/script.py.mako | 24 ++ .../migration_harbor/versions/1_5_0.py | 39 +++ .../migration_harbor/versions/1_6_0.py | 54 ++++ tools/migration/db/run.sh | 10 +- tools/migration/db/util/alembic.sh | 17 +- tools/migration/db/util/mysql.sh | 2 +- 29 files changed, 575 insertions(+), 19 deletions(-) rename tools/migration/db/alembic/{ => mysql}/alembic.sql (100%) rename tools/migration/db/alembic/{ => mysql}/alembic.tpl (95%) rename tools/migration/db/alembic/{ => mysql}/db_meta.py (99%) rename tools/migration/db/alembic/{ => mysql}/migration_harbor/env.py (100%) rename tools/migration/db/alembic/{ => mysql}/migration_harbor/script.py.mako (100%) rename tools/migration/db/alembic/{ => mysql}/migration_harbor/versions/0_1_1.py (100%) rename tools/migration/db/alembic/{ => mysql}/migration_harbor/versions/0_3_0.py (100%) rename tools/migration/db/alembic/{ => mysql}/migration_harbor/versions/0_4_0.py (100%) rename tools/migration/db/alembic/{ => mysql}/migration_harbor/versions/1_2_0.py (100%) rename tools/migration/db/alembic/{ => mysql}/migration_harbor/versions/1_3_0.py (100%) rename tools/migration/db/alembic/{ => mysql}/migration_harbor/versions/1_4_0.py (100%) rename tools/migration/db/alembic/{ => mysql}/migration_harbor/versions/1_5_0.py (100%) create mode 100644 tools/migration/db/alembic/postgres/alembic.tpl create mode 100644 tools/migration/db/alembic/postgres/db_meta.py create mode 100644 tools/migration/db/alembic/postgres/migration_harbor/README create mode 100644 tools/migration/db/alembic/postgres/migration_harbor/env.py create mode 100644 tools/migration/db/alembic/postgres/migration_harbor/script.py.mako create mode 100644 tools/migration/db/alembic/postgres/migration_harbor/versions/1_5_0.py create mode 100644 tools/migration/db/alembic/postgres/migration_harbor/versions/1_6_0.py diff --git a/Makefile b/Makefile index 92071762c..ce7ca1fb2 100644 --- a/Makefile +++ b/Makefile @@ -102,7 +102,7 @@ NOTARYVERSION=v0.5.1 MARIADBVERSION=$(VERSIONTAG) CLAIRVERSION=v2.0.1 CLAIRDBVERSION=$(VERSIONTAG) -MIGRATORVERSION=v1.5.0 +MIGRATORVERSION=v1.6.0 REDISVERSION=$(VERSIONTAG) #clarity parameters diff --git a/make/migrations/postgresql/0001_initial_schema.up.sql b/make/migrations/postgresql/0001_initial_schema.up.sql index 3e58d5029..57179eff5 100644 --- a/make/migrations/postgresql/0001_initial_schema.up.sql +++ b/make/migrations/postgresql/0001_initial_schema.up.sql @@ -341,5 +341,5 @@ CREATE TABLE IF NOT EXISTS alembic_version ( version_num varchar(32) NOT NULL ); -insert into alembic_version values ('1.5.0'); +insert into alembic_version values ('1.6.0'); diff --git a/src/common/dao/version.go b/src/common/dao/version.go index b2981dc4b..8d4e3423a 100644 --- a/src/common/dao/version.go +++ b/src/common/dao/version.go @@ -20,7 +20,7 @@ import ( const ( // SchemaVersion is the version of database schema - SchemaVersion = "1.5.0" + SchemaVersion = "1.6.0" ) // GetSchemaVersion return the version of database schema diff --git a/src/ui_ng/lib/package.json b/src/ui_ng/lib/package.json index f779fd0d5..8475c34c7 100644 --- a/src/ui_ng/lib/package.json +++ b/src/ui_ng/lib/package.json @@ -1,6 +1,6 @@ { "name": "harbor-ui", - "version": "0.7.19-dev.8", + "version": "0.7.19-dev.9", "description": "Harbor shared UI components based on Clarity and Angular4", "author": "VMware", "module": "index.js", diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index 477acf2b0..9eec0f086 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -49,7 +49,7 @@ "bootstrap": "4.0.0-alpha.5", "codelyzer": "~2.0.0-beta.4", "enhanced-resolve": "^3.0.0", - "harbor-ui": "0.7.19-test-16", + "harbor-ui": "0.7.19-dev.9", "jasmine-core": "2.4.1", "jasmine-spec-reporter": "2.5.0", "karma": "~1.7.0", diff --git a/tools/migration/Dockerfile b/tools/migration/Dockerfile index e7ac8e43d..507fe7a53 100644 --- a/tools/migration/Dockerfile +++ b/tools/migration/Dockerfile @@ -8,7 +8,7 @@ RUN tdnf distro-sync -y || echo \ && groupadd -r -g 10000 mysql && useradd --no-log-init -r -g 10000 -u 10000 mysql \ && tdnf install -y mariadb-server mariadb mariadb-devel python2 python2-devel python-pip gcc \ linux-api-headers glibc-devel binutils zlib-devel openssl-devel postgresql >> /dev/null\ - && pip install mysqlclient alembic \ + && pip install mysqlclient alembic psycopg2 \ && mkdir /docker-entrypoint-initdb.d /docker-entrypoint-updatedb.d \ && rm -fr /var/lib/mysql \ && mkdir -p /var/lib/mysql /var/run/mysqld \ diff --git a/tools/migration/cfg/run.py b/tools/migration/cfg/run.py index b6fdb94ab..ab1205cb7 100644 --- a/tools/migration/cfg/run.py +++ b/tools/migration/cfg/run.py @@ -13,7 +13,7 @@ import shutil import sys def main(): - target_version = '1.5.0' + target_version = '1.6.0' parser = argparse.ArgumentParser(description='migrator of harbor.cfg') parser.add_argument('--input', '-i', action="store", dest='input_path', required=True, help='The path to the old harbor.cfg that provides input value, this required value') parser.add_argument('--output','-o', action="store", dest='output_path', required=False, help='The path of the migrated harbor.cfg, if not set the input file will be overwritten') diff --git a/tools/migration/db/alembic/alembic.sql b/tools/migration/db/alembic/mysql/alembic.sql similarity index 100% rename from tools/migration/db/alembic/alembic.sql rename to tools/migration/db/alembic/mysql/alembic.sql diff --git a/tools/migration/db/alembic/alembic.tpl b/tools/migration/db/alembic/mysql/alembic.tpl similarity index 95% rename from tools/migration/db/alembic/alembic.tpl rename to tools/migration/db/alembic/mysql/alembic.tpl index 3e37dfeaf..fa7a75c8a 100644 --- a/tools/migration/db/alembic/alembic.tpl +++ b/tools/migration/db/alembic/mysql/alembic.tpl @@ -3,7 +3,7 @@ echo " [alembic] # path to migration scripts -script_location = /harbor-migration/db/alembic/migration_harbor +script_location = /harbor-migration/db/alembic/mysql/migration_harbor # template used to generate migration files # file_template = %%(rev)s_%%(slug)s diff --git a/tools/migration/db/alembic/db_meta.py b/tools/migration/db/alembic/mysql/db_meta.py similarity index 99% rename from tools/migration/db/alembic/db_meta.py rename to tools/migration/db/alembic/mysql/db_meta.py index c012f9ff3..8d1635e49 100644 --- a/tools/migration/db/alembic/db_meta.py +++ b/tools/migration/db/alembic/mysql/db_meta.py @@ -265,7 +265,7 @@ class HarborLabel(Base): class HarborResourceLabel(Base): __tablename__ = 'harbor_resource_label' - id = sa.Column(sa.Integer, nullable=False, primary_key=True) + id = sa.Column(sa.Integer, primary_key=True) label_id = sa.Column(sa.Integer, nullable=False) resource_id = sa.Column(sa.Integer) resource_name = sa.Column(sa.String(256)) diff --git a/tools/migration/db/alembic/migration_harbor/env.py b/tools/migration/db/alembic/mysql/migration_harbor/env.py similarity index 100% rename from tools/migration/db/alembic/migration_harbor/env.py rename to tools/migration/db/alembic/mysql/migration_harbor/env.py diff --git a/tools/migration/db/alembic/migration_harbor/script.py.mako b/tools/migration/db/alembic/mysql/migration_harbor/script.py.mako similarity index 100% rename from tools/migration/db/alembic/migration_harbor/script.py.mako rename to tools/migration/db/alembic/mysql/migration_harbor/script.py.mako diff --git a/tools/migration/db/alembic/migration_harbor/versions/0_1_1.py b/tools/migration/db/alembic/mysql/migration_harbor/versions/0_1_1.py similarity index 100% rename from tools/migration/db/alembic/migration_harbor/versions/0_1_1.py rename to tools/migration/db/alembic/mysql/migration_harbor/versions/0_1_1.py diff --git a/tools/migration/db/alembic/migration_harbor/versions/0_3_0.py b/tools/migration/db/alembic/mysql/migration_harbor/versions/0_3_0.py similarity index 100% rename from tools/migration/db/alembic/migration_harbor/versions/0_3_0.py rename to tools/migration/db/alembic/mysql/migration_harbor/versions/0_3_0.py diff --git a/tools/migration/db/alembic/migration_harbor/versions/0_4_0.py b/tools/migration/db/alembic/mysql/migration_harbor/versions/0_4_0.py similarity index 100% rename from tools/migration/db/alembic/migration_harbor/versions/0_4_0.py rename to tools/migration/db/alembic/mysql/migration_harbor/versions/0_4_0.py diff --git a/tools/migration/db/alembic/migration_harbor/versions/1_2_0.py b/tools/migration/db/alembic/mysql/migration_harbor/versions/1_2_0.py similarity index 100% rename from tools/migration/db/alembic/migration_harbor/versions/1_2_0.py rename to tools/migration/db/alembic/mysql/migration_harbor/versions/1_2_0.py diff --git a/tools/migration/db/alembic/migration_harbor/versions/1_3_0.py b/tools/migration/db/alembic/mysql/migration_harbor/versions/1_3_0.py similarity index 100% rename from tools/migration/db/alembic/migration_harbor/versions/1_3_0.py rename to tools/migration/db/alembic/mysql/migration_harbor/versions/1_3_0.py diff --git a/tools/migration/db/alembic/migration_harbor/versions/1_4_0.py b/tools/migration/db/alembic/mysql/migration_harbor/versions/1_4_0.py similarity index 100% rename from tools/migration/db/alembic/migration_harbor/versions/1_4_0.py rename to tools/migration/db/alembic/mysql/migration_harbor/versions/1_4_0.py diff --git a/tools/migration/db/alembic/migration_harbor/versions/1_5_0.py b/tools/migration/db/alembic/mysql/migration_harbor/versions/1_5_0.py similarity index 100% rename from tools/migration/db/alembic/migration_harbor/versions/1_5_0.py rename to tools/migration/db/alembic/mysql/migration_harbor/versions/1_5_0.py diff --git a/tools/migration/db/alembic/postgres/alembic.tpl b/tools/migration/db/alembic/postgres/alembic.tpl new file mode 100644 index 000000000..7b67f902d --- /dev/null +++ b/tools/migration/db/alembic/postgres/alembic.tpl @@ -0,0 +1,75 @@ +echo " +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = /harbor-migration/db/alembic/postgres/migration_harbor + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to migration_harbor/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat migration_harbor/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql://$PGSQL_USR:$DB_PWD@localhost:5432/registry + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S" \ No newline at end of file diff --git a/tools/migration/db/alembic/postgres/db_meta.py b/tools/migration/db/alembic/postgres/db_meta.py new file mode 100644 index 000000000..fe0f3557e --- /dev/null +++ b/tools/migration/db/alembic/postgres/db_meta.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relationship +from sqlalchemy.dialects import postgresql +from sqlalchemy.sql import func +import datetime + +Base = declarative_base() + + +class User(Base): + __tablename__ = 'harbor_user' + + user_id = sa.Column(sa.Integer, primary_key=True) + username = sa.Column(sa.String(255), unique=True) + email = sa.Column(sa.String(255), unique=True) + password = sa.Column(sa.String(40), nullable=False) + realname = sa.Column(sa.String(255), nullable=False) + comment = sa.Column(sa.String(30)) + deleted = sa.Column(sa.Boolean, nullable=False, server_default='false') + reset_uuid = sa.Column(sa.String(40)) + salt = sa.Column(sa.String(40)) + sysadmin_flag = sa.Column(sa.Boolean, nullable=False, server_default='false') + creation_time = sa.Column(sa.TIMESTAMP) + update_time = sa.Column(sa.TIMESTAMP) + + +class UserGroup(Base): + __tablename__ = 'user_group' + + id = sa.Column(sa.Integer, primary_key=True) + group_name = sa.Column(sa.String(255), nullable = False) + group_type = sa.Column(sa.SmallInteger, server_default=sa.text("'0'")) + ldap_group_dn = sa.Column(sa.String(512), nullable=False) + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + +class Properties(Base): + __tablename__ = 'properties' + + id = sa.Column(sa.Integer, primary_key=True) + k = sa.Column(sa.String(64), unique=True) + v = sa.Column(sa.String(128), nullable = False) + + +class ProjectMember(Base): + __tablename__ = 'project_member' + + id = sa.Column(sa.Integer, primary_key=True) + project_id = sa.Column(sa.Integer(), nullable=False) + entity_id = sa.Column(sa.Integer(), nullable=False) + entity_type = sa.Column(sa.String(1), nullable=False) + role = sa.Column(sa.Integer(), nullable = False) + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + __table_args__ = (sa.UniqueConstraint('project_id', 'entity_id', 'entity_type', name='unique_name_and_scope'),) + + +class UserProjectRole(Base): + __tablename__ = 'user_project_role' + + upr_id = sa.Column(sa.Integer(), primary_key = True) + user_id = sa.Column(sa.Integer(), sa.ForeignKey('user.user_id')) + pr_id = sa.Column(sa.Integer(), sa.ForeignKey('project_role.pr_id')) + project_role = relationship("ProjectRole") + + +class ProjectRole(Base): + __tablename__ = 'project_role' + + pr_id = sa.Column(sa.Integer(), primary_key = True) + project_id = sa.Column(sa.Integer(), nullable = False) + role_id = sa.Column(sa.Integer(), nullable = False) + sa.ForeignKeyConstraint(['role_id'], [u'role.role_id']) + sa.ForeignKeyConstraint(['project_id'], [u'project.project_id']) + + +class Access(Base): + __tablename__ = 'access' + + access_id = sa.Column(sa.Integer(), primary_key = True) + access_code = sa.Column(sa.String(1)) + comment = sa.Column(sa.String(30)) + + +class Role(Base): + __tablename__ = 'role' + + role_id = sa.Column(sa.Integer, primary_key=True) + role_mask = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) + role_code = sa.Column(sa.String(20)) + name = sa.Column(sa.String(20)) + + +class Project(Base): + __tablename__ = 'project' + + project_id = sa.Column(sa.Integer, primary_key=True) + owner_id = sa.Column(sa.ForeignKey(u'harbor_user.user_id'), nullable=False, index=True) + name = sa.Column(sa.String(255), nullable=False, unique=True) + creation_time = sa.Column(sa.TIMESTAMP) + update_time = sa.Column(sa.TIMESTAMP) + deleted = sa.Column(sa.Boolean, nullable=False, server_default='false') + owner = relationship(u'User') + + +class ProjectMetadata(Base): + __tablename__ = 'project_metadata' + + id = sa.Column(sa.Integer, primary_key=True) + project_id = sa.Column(sa.ForeignKey(u'project.project_id'), nullable=False) + name = sa.Column(sa.String(255), nullable=False) + value = sa.Column(sa.String(255)) + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + deleted = sa.Column(sa.Boolean, nullable=False, server_default='false') + + __table_args__ = (sa.UniqueConstraint('project_id', 'name', name='unique_project_id_and_name'),) + + +class ReplicationPolicy(Base): + __tablename__ = "replication_policy" + + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(256)) + project_id = sa.Column(sa.Integer, nullable=False) + target_id = sa.Column(sa.Integer, nullable=False) + enabled = sa.Column(sa.Boolean, nullable=False, server_default='true') + description = sa.Column(sa.Text) + cron_str = sa.Column(sa.String(256)) + filters = sa.Column(sa.String(1024)) + replicate_deletion = sa.Column(sa.Boolean, nullable=False, server_default='false') + start_time = sa.Column(sa.TIMESTAMP) + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + +class ReplicationTarget(Base): + __tablename__ = "replication_target" + + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(64)) + url = sa.Column(sa.String(64)) + username = sa.Column(sa.String(255)) + password = sa.Column(sa.String(128)) + target_type = sa.Column(sa.SmallInteger, nullable=False, server_default=sa.text("'0'")) + insecure = sa.Column(sa.Boolean, nullable=False, server_default='false') + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + +class ReplicationJob(Base): + __tablename__ = "replication_job" + + id = sa.Column(sa.Integer, primary_key=True) + status = sa.Column(sa.String(64), nullable=False) + policy_id = sa.Column(sa.Integer, nullable=False) + repository = sa.Column(sa.String(256), nullable=False) + operation = sa.Column(sa.String(64), nullable=False) + tags = sa.Column(sa.String(16384)) + job_uuid = sa.Column(sa.String(64)) + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + __table_args__ = (sa.Index('policy', 'policy_id'),) + + +class ReplicationImmediateTrigger(Base): + __tablename__ = 'replication_immediate_trigger' + + id = sa.Column(sa.Integer, primary_key=True) + policy_id = sa.Column(sa.Integer, nullable=False) + namespace = sa.Column(sa.String(256), nullable=False) + on_push = sa.Column(sa.Boolean, nullable=False, server_default='false') + on_deletion = sa.Column(sa.Boolean, nullable=False, server_default='false') + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + +class Repository(Base): + __tablename__ = "repository" + + repository_id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(255), nullable=False, unique=True) + project_id = sa.Column(sa.Integer, nullable=False) + owner_id = sa.Column(sa.Integer, nullable=False) + description = sa.Column(sa.Text) + pull_count = sa.Column(sa.Integer,server_default=sa.text("'0'"), nullable=False) + star_count = sa.Column(sa.Integer,server_default=sa.text("'0'"), nullable=False) + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + +class AccessLog(Base): + __tablename__ = "access_log" + + log_id = sa.Column(sa.Integer, primary_key=True) + username = sa.Column(sa.String(255), nullable=False) + project_id = sa.Column(sa.Integer, nullable=False) + repo_name = sa.Column(sa.String(256)) + repo_tag = sa.Column(sa.String(128)) + GUID = sa.Column(sa.String(64)) + operation = sa.Column(sa.String(20)) + op_time = sa.Column(sa.TIMESTAMP) + + __table_args__ = (sa.Index('project_id', "op_time"),) + + +class ImageScanJob(Base): + __tablename__ = "img_scan_job" + + id = sa.Column(sa.Integer, nullable=False, primary_key=True) + status = sa.Column(sa.String(64), nullable=False) + repository = sa.Column(sa.String(256), nullable=False) + tag = sa.Column(sa.String(128), nullable=False) + digest = sa.Column(sa.String(128)) + job_uuid = sa.Column(sa.String(64)) + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + +class ImageScanOverview(Base): + __tablename__ = "img_scan_overview" + + id = sa.Column(sa.Integer, nullable=False, primary_key=True) + image_digest = sa.Column(sa.String(128), nullable=False) + scan_job_id = sa.Column(sa.Integer, nullable=False) + severity = sa.Column(sa.Integer, nullable=False, server_default=sa.text("'0'")) + components_overview = sa.Column(sa.String(2048)) + details_key = sa.Column(sa.String(128)) + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + +class ClairVulnTimestamp(Base): + __tablename__ = "clair_vuln_timestamp" + + id = sa.Column(sa.Integer, nullable=False, primary_key=True) + namespace = sa.Column(sa.String(128), nullable=False, unique=True) + last_update = sa.Column(sa.TIMESTAMP) + + +class HarborLabel(Base): + __tablename__ = "harbor_label" + + id = sa.Column(sa.Integer, nullable=False, primary_key=True) + name = sa.Column(sa.String(128), nullable=False) + description = sa.Column(sa.Text) + color = sa.Column(sa.String(16)) + level = sa.Column(sa.String(1), nullable=False) + scope = sa.Column(sa.String(1), nullable=False) + project_id = sa.Column(sa.Integer, nullable=False) + deleted = sa.Column(sa.Boolean, nullable=False, server_default='false') + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + __table_args__ = (sa.UniqueConstraint('name', 'scope', 'project_id', name='unique_label'),) + + +class HarborResourceLabel(Base): + __tablename__ = 'harbor_resource_label' + + id = sa.Column(sa.Integer, primary_key=True) + label_id = sa.Column(sa.Integer, nullable=False) + resource_id = sa.Column(sa.Integer) + resource_name = sa.Column(sa.String(256)) + resource_type = sa.Column(sa.String(1), nullable=False) + creation_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + update_time = sa.Column(sa.TIMESTAMP, server_default=sa.text("'now'::timestamp")) + + + __table_args__ = (sa.UniqueConstraint('label_id', 'resource_id', 'resource_name', 'resource_type', name='unique_label_resource'),) + + +class SchemaMigrations(Base): + __tablename__ = 'schema_migrations' + + version = sa.Column(sa.BigInteger, primary_key=True) + dirty = sa.Column(sa.Boolean, nullable=False) \ No newline at end of file diff --git a/tools/migration/db/alembic/postgres/migration_harbor/README b/tools/migration/db/alembic/postgres/migration_harbor/README new file mode 100644 index 000000000..98e4f9c44 --- /dev/null +++ b/tools/migration/db/alembic/postgres/migration_harbor/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/tools/migration/db/alembic/postgres/migration_harbor/env.py b/tools/migration/db/alembic/postgres/migration_harbor/env.py new file mode 100644 index 000000000..058378b9d --- /dev/null +++ b/tools/migration/db/alembic/postgres/migration_harbor/env.py @@ -0,0 +1,70 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/tools/migration/db/alembic/postgres/migration_harbor/script.py.mako b/tools/migration/db/alembic/postgres/migration_harbor/script.py.mako new file mode 100644 index 000000000..2c0156303 --- /dev/null +++ b/tools/migration/db/alembic/postgres/migration_harbor/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/tools/migration/db/alembic/postgres/migration_harbor/versions/1_5_0.py b/tools/migration/db/alembic/postgres/migration_harbor/versions/1_5_0.py new file mode 100644 index 000000000..d39c8ba20 --- /dev/null +++ b/tools/migration/db/alembic/postgres/migration_harbor/versions/1_5_0.py @@ -0,0 +1,39 @@ +# Copyright (c) 2008-2016 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. + +"""Empty version + +Revision ID: 1.5.0 +Revises: +Create Date: 2018-6-26 + +""" + +# revision identifiers, used by Alembic. +revision = '1.5.0' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + """ + update schema&data + """ + pass + +def downgrade(): + """ + Downgrade has been disabled. + """ + pass \ No newline at end of file diff --git a/tools/migration/db/alembic/postgres/migration_harbor/versions/1_6_0.py b/tools/migration/db/alembic/postgres/migration_harbor/versions/1_6_0.py new file mode 100644 index 000000000..5a282d16a --- /dev/null +++ b/tools/migration/db/alembic/postgres/migration_harbor/versions/1_6_0.py @@ -0,0 +1,54 @@ +# Copyright (c) 2008-2016 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. + +"""1.5.0 to 1.6.0 + +Revision ID: 1.6.0 +Revises: +Create Date: 2018-6-26 + +""" + +# revision identifiers, used by Alembic. +revision = '1.6.0' +down_revision = '1.5.0' +branch_labels = None +depends_on = None + +from alembic import op +from db_meta import * + +Session = sessionmaker() + +def upgrade(): + """ + update schema&data + """ + bind = op.get_bind() + session = Session(bind=bind) + + ## Add column deleted to harbor_label + op.add_column('harbor_label', sa.Column('deleted', sa.Boolean, nullable=False, server_default='false')) + + ## Add schema_migration then insert data + SchemaMigrations.__table__.create(bind) + session.add(SchemaMigrations(version=1, dirty=False)) + + session.commit() + +def downgrade(): + """ + Downgrade has been disabled. + """ + pass diff --git a/tools/migration/db/run.sh b/tools/migration/db/run.sh index 5084e41e5..5a6c7d0a7 100755 --- a/tools/migration/db/run.sh +++ b/tools/migration/db/run.sh @@ -198,13 +198,17 @@ function up_harbor { psql -U $PGSQL_USR -f /harbor-migration/db/schema/notaryserver_init.pgsql psql -U $PGSQL_USR -f /harbor-migration/db/schema/notarysigner_init.pgsql - ## it needs to call the alembic_up to target, disable it as it's now unsupported. - #alembic_up $target_version - stop_pgsql $PGSQL_USR stop_mysql $DB_USR $DB_PWD + ## it needs to call the alembic_up to target, disable it as it's now unsupported. + alembic_up pgsql $target_version + stop_pgsql $PGSQL_USR + rm -rf /var/lib/mysql/* cp -rf $PGDATA/* /var/lib/mysql + + ## Chmod 700 to DB data directory + chmod 700 /var/lib/mysql return 0 fi fi diff --git a/tools/migration/db/util/alembic.sh b/tools/migration/db/util/alembic.sh index 112cb7635..a150234e4 100644 --- a/tools/migration/db/util/alembic.sh +++ b/tools/migration/db/util/alembic.sh @@ -19,17 +19,22 @@ set -e function alembic_up { local db_type="$1" local target_version="$2" - - export PYTHONPATH=$PYTHONPATH:/harbor-migration/db/alembic if [ $db_type = "mysql" ]; then - source /harbor-migration/db/alembic/alembic.tpl > /harbor-migration/db/alembic/alembic.ini + export PYTHONPATH=/harbor-migration/db/alembic/mysql + source /harbor-migration/db/alembic/mysql/alembic.tpl > /harbor-migration/db/alembic/mysql/alembic.ini echo "Performing upgrade $target_version..." - alembic -c /harbor-migration/db/alembic/alembic.ini current - alembic -c /harbor-migration/db/alembic/alembic.ini upgrade $target_version - alembic -c /harbor-migration/db/alembic/alembic.ini current + alembic -c /harbor-migration/db/alembic/mysql/alembic.ini current + alembic -c /harbor-migration/db/alembic/mysql/alembic.ini upgrade $target_version + alembic -c /harbor-migration/db/alembic/mysql/alembic.ini current elif [ $db_type = "pgsql" ]; then + export PYTHONPATH=/harbor-migration/db/alembic/postgres echo "TODO: add support for pgsql." + source /harbor-migration/db/alembic/postgres/alembic.tpl > /harbor-migration/db/alembic/postgres/alembic.ini + echo "Performing upgrade $target_version..." + alembic -c /harbor-migration/db/alembic/postgres/alembic.ini current + alembic -c /harbor-migration/db/alembic/postgres/alembic.ini upgrade $target_version + alembic -c /harbor-migration/db/alembic/postgres/alembic.ini current else echo "Unsupported DB type." exit 1 diff --git a/tools/migration/db/util/mysql.sh b/tools/migration/db/util/mysql.sh index 04a40821e..2c8f4dc9c 100644 --- a/tools/migration/db/util/mysql.sh +++ b/tools/migration/db/util/mysql.sh @@ -76,7 +76,7 @@ function get_version_mysql { if [[ $(mysql $DBCNF -N -s -e "select count(*) from information_schema.tables \ where table_schema='registry' and table_name='alembic_version';") -eq 0 ]]; then echo "table alembic_version does not exist. Trying to initial alembic_version." - mysql $DBCNF < ./alembic.sql + mysql $DBCNF < /harbor-migration/db/alembic/mysql/alembic.sql #compatible with version 0.1.0 and 0.1.1 if [[ $(mysql $DBCNF -N -s -e "select count(*) from information_schema.tables \ where table_schema='registry' and table_name='properties'") -eq 0 ]]; then From cb0acbace44aeb67c8e022362bf46b36c10ef4b2 Mon Sep 17 00:00:00 2001 From: Daniel Jiang Date: Tue, 3 Jul 2018 11:23:56 +0800 Subject: [PATCH 06/32] Restrict the CPU usage of Clair (#5217) This commit fixes #5072 Due to an issue in bzr, Clair container may consume a lot of CPU resource while updating the vuln data. This commit mitigates the impact by setting the cpu_quota of clair container. (default value of cpu_period is 100000 in v2 docker-compose template) --- make/docker-compose.clair.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make/docker-compose.clair.tpl b/make/docker-compose.clair.tpl index 74fad5a29..62bd25dc1 100644 --- a/make/docker-compose.clair.tpl +++ b/make/docker-compose.clair.tpl @@ -22,7 +22,7 @@ services: container_name: clair image: vmware/clair-photon:__clair_version__ restart: always - cpu_quota: 150000 + cpu_quota: 50000 depends_on: - postgresql volumes: From 72dfdd552fb7d5e4157435c30c7ba6c0ad38a7a6 Mon Sep 17 00:00:00 2001 From: "Deng, Qian" Date: Thu, 17 May 2018 16:50:49 +0800 Subject: [PATCH 07/32] Add ldap serach 1. Add group management 2. Add rewrite import user to member ui 3. Add import group to member 4. Add new items in configuration page --- .travis.yml | 4 +- docs/compile_guide.md | 6 +- make/dev/nodeclarity/entrypoint.sh | 2 +- src/ui_ng/angular-cli.json | 2 +- src/ui_ng/lib/src/config/config.ts | 8 + .../confirmation-batch-message.ts | 19 +- src/ui_ng/src/app/app.module.ts | 4 +- src/ui_ng/src/app/base/base.module.ts | 4 +- .../harbor-shell/harbor-shell.component.html | 7 + .../harbor-shell/harbor-shell.component.ts | 14 +- .../config/auth/config-auth.component.html | 171 ++++++--- .../config/auth/config-auth.component.scss | 3 + src/ui_ng/src/app/config/config.component.ts | 2 +- .../add-group-modal.component.html | 43 +++ .../add-group-modal.component.scss | 0 .../add-group-modal.component.spec.ts | 25 ++ .../add-group-modal.component.ts | 109 ++++++ src/ui_ng/src/app/group/group.component.html | 40 ++ src/ui_ng/src/app/group/group.component.scss | 45 +++ .../src/app/group/group.component.spec.ts | 25 ++ src/ui_ng/src/app/group/group.component.ts | 166 ++++++++ src/ui_ng/src/app/group/group.module.ts | 27 ++ src/ui_ng/src/app/group/group.service.spec.ts | 15 + src/ui_ng/src/app/group/group.service.ts | 81 ++++ src/ui_ng/src/app/group/group.ts | 12 + src/ui_ng/src/app/harbor-routing.module.ts | 6 + .../member/add-group/add-group.component.html | 104 +++++ .../member/add-group/add-group.component.scss | 18 + .../add-group/add-group.component.spec.ts | 25 ++ .../member/add-group/add-group.component.ts | 167 ++++++++ .../add-member/add-member.component.html | 19 +- .../add-member/add-member.component.scss | 2 +- .../member/add-member/add-member.component.ts | 84 ++-- .../app/project/member/member.component.html | 27 +- .../app/project/member/member.component.scss | 6 + .../app/project/member/member.component.ts | 358 +++++++++--------- .../src/app/project/member/member.service.ts | 38 +- src/ui_ng/src/app/project/project.module.ts | 6 +- src/ui_ng/src/app/shared/shared.const.ts | 5 + src/ui_ng/src/app/shared/shared.module.ts | 7 +- src/ui_ng/src/app/user/user.component.html | 9 +- src/ui_ng/src/app/user/user.service.ts | 28 +- src/ui_ng/src/app/user/user.ts | 21 +- src/ui_ng/src/i18n/lang/en-us-lang.json | 61 ++- src/ui_ng/src/i18n/lang/es-es-lang.json | 61 ++- src/ui_ng/src/i18n/lang/fr-fr-lang.json | 60 ++- src/ui_ng/src/i18n/lang/zh-cn-lang.json | 65 +++- src/ui_ng/src/{styles.scss => styles.css} | 0 tests/resources/Harbor-Util.robot | 2 +- 49 files changed, 1648 insertions(+), 365 deletions(-) create mode 100644 src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.html create mode 100644 src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.scss create mode 100644 src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.spec.ts create mode 100644 src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.ts create mode 100644 src/ui_ng/src/app/group/group.component.html create mode 100644 src/ui_ng/src/app/group/group.component.scss create mode 100644 src/ui_ng/src/app/group/group.component.spec.ts create mode 100644 src/ui_ng/src/app/group/group.component.ts create mode 100644 src/ui_ng/src/app/group/group.module.ts create mode 100644 src/ui_ng/src/app/group/group.service.spec.ts create mode 100644 src/ui_ng/src/app/group/group.service.ts create mode 100644 src/ui_ng/src/app/group/group.ts create mode 100644 src/ui_ng/src/app/project/member/add-group/add-group.component.html create mode 100644 src/ui_ng/src/app/project/member/add-group/add-group.component.scss create mode 100644 src/ui_ng/src/app/project/member/add-group/add-group.component.spec.ts create mode 100644 src/ui_ng/src/app/project/member/add-group/add-group.component.ts rename src/ui_ng/src/{styles.scss => styles.css} (100%) diff --git a/.travis.yml b/.travis.yml index 9d0b8d016..55a4c8659 100644 --- a/.travis.yml +++ b/.travis.yml @@ -79,7 +79,7 @@ script: - sudo mkdir -p /harbor - sudo mv ./VERSION /harbor/UIVERSION - sudo service postgresql stop - - sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 + - sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 - cat ./src/ui_ng/npm-ut-test-results - sudo ./tests/testprepare.sh - sudo make -f make/photon/Makefile _build_db _build_registry -e VERSIONTAG=dev -e CLAIRDBVERSION=dev -e REGISTRYVERSION=v2.6.2 @@ -105,7 +105,7 @@ script: - sudo rm -rf /data/config/* - sudo rm -rf /data/database/* - ls /data/cert - - sudo make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 NOTARYFLAG=true CLAIRFLAG=true + - sudo make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 NOTARYFLAG=true CLAIRFLAG=true - sleep 10 - docker ps - ./tests/validatecontainers.sh diff --git a/docs/compile_guide.md b/docs/compile_guide.md index 475b0cdd3..a2d0560c4 100644 --- a/docs/compile_guide.md +++ b/docs/compile_guide.md @@ -50,19 +50,19 @@ You can compile the code by one of the three approaches: * Build, install and bring up Harbor without Notary: ```sh - $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 + $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 ``` * Build, install and bring up Harbor with Notary: ```sh - $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 NOTARYFLAG=true + $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 NOTARYFLAG=true ``` * Build, install and bring up Harbor with Clair: ```sh - $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.1 CLAIRFLAG=true + $ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.4.0 CLAIRFLAG=true ``` #### II. Compile code with your own Golang environment, then build Harbor diff --git a/make/dev/nodeclarity/entrypoint.sh b/make/dev/nodeclarity/entrypoint.sh index b4899bb6d..bec32011d 100644 --- a/make/dev/nodeclarity/entrypoint.sh +++ b/make/dev/nodeclarity/entrypoint.sh @@ -34,7 +34,7 @@ sed -i 's/* as//g' src/app/shared/gauge/gauge.component.js cp ./dist/build.min.js ../ui/static/ cp -r ./src/i18n/ ../ui/static/ -cp ./src/styles.scss ../ui/static/ +cp ./src/styles.css ../ui/static/ cp -r ./src/images/ ../ui/static/ cp ./src/setting.json ../ui/static/ diff --git a/src/ui_ng/angular-cli.json b/src/ui_ng/angular-cli.json index 93021a4b1..87732b1de 100644 --- a/src/ui_ng/angular-cli.json +++ b/src/ui_ng/angular-cli.json @@ -20,7 +20,7 @@ "styles": [ "../node_modules/clarity-icons/clarity-icons.min.css", "../node_modules/clarity-ui/clarity-ui.min.css", - "styles.scss" + "styles.css" ], "scripts": [ "../node_modules/core-js/client/shim.min.js", diff --git a/src/ui_ng/lib/src/config/config.ts b/src/ui_ng/lib/src/config/config.ts index 07631722a..29e4f0feb 100644 --- a/src/ui_ng/lib/src/config/config.ts +++ b/src/ui_ng/lib/src/config/config.ts @@ -65,6 +65,10 @@ export class Configuration { ldap_uid: StringValueItem; ldap_url: StringValueItem; ldap_verify_cert: BoolValueItem; + ldap_group_base_dn: StringValueItem; + ldap_group_search_filter: StringValueItem; + ldap_group_attribute_name: StringValueItem; + ldap_group_search_scope: NumberValueItem; uaa_client_id: StringValueItem; uaa_client_secret?: StringValueItem; uaa_endpoint: StringValueItem; @@ -96,6 +100,10 @@ export class Configuration { this.ldap_uid = new StringValueItem("", true); this.ldap_url = new StringValueItem("", true); this.ldap_verify_cert = new BoolValueItem(true, true); + this.ldap_group_base_dn = new StringValueItem("", true); + this.ldap_group_search_filter = new StringValueItem("", true); + this.ldap_group_attribute_name = new StringValueItem("", true); + this.ldap_group_search_scope = new NumberValueItem(0, true); this.uaa_client_id = new StringValueItem("", true); this.uaa_client_secret = new StringValueItem("", true); this.uaa_endpoint = new StringValueItem("", true); diff --git a/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts b/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts index b4b160ee1..70934be98 100644 --- a/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts +++ b/src/ui_ng/lib/src/confirmation-dialog/confirmation-batch-message.ts @@ -4,6 +4,7 @@ */ export class BatchInfo { + id?: number; name: string; status: string; loading: boolean; @@ -17,11 +18,17 @@ export class BatchInfo { } } -export function BathInfoChanges(list: BatchInfo, status: string, loading = false, errStatus = false, errorInfo = '') { - list.status = status; - list.loading = loading; - list.errorState = errStatus; - list.errorInfo = errorInfo; - return list; +export function BathInfoChanges(batchInfo: BatchInfo, status: string, loading = false, errStatus = false, errorInfo = '') { + batchInfo.status = status; + batchInfo.loading = loading; + batchInfo.errorState = errStatus; + batchInfo.errorInfo = errorInfo; + return batchInfo; +} + +export enum BatchOperations { + Idle, + Delete, + ChangeRole } diff --git a/src/ui_ng/src/app/app.module.ts b/src/ui_ng/src/app/app.module.ts index 31da2cc0b..7d7b0023b 100644 --- a/src/ui_ng/src/app/app.module.ts +++ b/src/ui_ng/src/app/app.module.ts @@ -48,7 +48,9 @@ export function getCurrentLanguage(translateService: TranslateService) { BaseModule, AccountModule, HarborRoutingModule, - ConfigurationModule, + ConfigurationModule + ], + exports: [ ], providers: [ AppConfigService, diff --git a/src/ui_ng/src/app/base/base.module.ts b/src/ui_ng/src/app/base/base.module.ts index 48b894c69..dffa8751c 100644 --- a/src/ui_ng/src/app/base/base.module.ts +++ b/src/ui_ng/src/app/base/base.module.ts @@ -19,6 +19,7 @@ import { ProjectModule } from '../project/project.module'; import { UserModule } from '../user/user.module'; import { AccountModule } from '../account/account.module'; import { RepositoryModule } from '../repository/repository.module'; +import { GroupModule } from '../group/group.module'; import { NavigatorComponent } from './navigator/navigator.component'; import { GlobalSearchComponent } from './global-search/global-search.component'; @@ -36,7 +37,8 @@ import { SearchTriggerService } from './global-search/search-trigger.service'; UserModule, AccountModule, RouterModule, - RepositoryModule + RepositoryModule, + GroupModule ], declarations: [ NavigatorComponent, diff --git a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html index a47bf1940..125bbcc9b 100644 --- a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html +++ b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html @@ -28,6 +28,13 @@ {{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}} + + + {{'SIDE_NAV.SYSTEM_MGMT.GROUP' | translate}} + diff --git a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.ts b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.ts index 3473602f3..3ae2fe5e6 100644 --- a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.ts +++ b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.ts @@ -13,6 +13,8 @@ // limitations under the License. import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs/Subscription'; +import { AppConfigService } from '../..//app-config.service'; import { ModalEvent } from '../modal-event'; import { modalEvents } from '../modal-events.const'; @@ -23,11 +25,7 @@ import { NavigatorComponent } from '../navigator/navigator.component'; import { SessionService } from '../../shared/session.service'; import { AboutDialogComponent } from '../../shared/about-dialog/about-dialog.component'; - import { SearchTriggerService } from '../global-search/search-trigger.service'; - -import { Subscription } from 'rxjs/Subscription'; - import { CommonRoutes } from '../../shared/shared.const'; @Component({ @@ -61,7 +59,8 @@ export class HarborShellComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private router: Router, private session: SessionService, - private searchTrigger: SearchTriggerService) { } + private searchTrigger: SearchTriggerService, + private appConfigService: AppConfigService) { } ngOnInit() { this.searchSub = this.searchTrigger.searchTriggerChan$.subscribe(searchEvt => { @@ -98,6 +97,11 @@ export class HarborShellComponent implements OnInit, OnDestroy { return account != null && account.has_admin_role; } + public get isLdapMode(): boolean { + let appConfig = this.appConfigService.getConfig(); + return appConfig.auth_mode === 'ldap_auth'; + } + public get isUserExisting(): boolean { let account = this.session.getCurrentUser(); return account != null; diff --git a/src/ui_ng/src/app/config/auth/config-auth.component.html b/src/ui_ng/src/app/config/auth/config-auth.component.html index 2b0464815..47cd4ffe4 100644 --- a/src/ui_ng/src/app/config/auth/config-auth.component.html +++ b/src/ui_ng/src/app/config/auth/config-auth.component.html @@ -19,50 +19,44 @@
- +
- +
- +
- +
- +
+ + + {{'TOOLTIP.ITEM_REQUIRED' | translate}} + + {{'CONFIG.TOOLTIP.LDAP_SEARCH_DN' | translate}} @@ -71,25 +65,23 @@
+ + + {{'TOOLTIP.ITEM_REQUIRED' | translate}} + +
- + {{'CONFIG.TOOLTIP.LDAP_BASE_DN' | translate}} @@ -98,25 +90,23 @@
+ + + {{'TOOLTIP.ITEM_REQUIRED' | translate}} + +
+
+ + + + + + {{'CONFIG.LDAP.LDAP_GROUP_BASE_DN_INFO' | translate}} + + +
+
+ + + + + + {{'CONFIG.LDAP.LDAP_GROUP_FILTER_INFO' | translate}} + + +
+
+ + + + + + {{'CONFIG.LDAP.LDAP_GROUP_GID_INFO' | translate}} + + +
+
+ +
+ +
+ + + + {{'CONFIG.LDAP.GROUP_SCOPE_INFO' | translate}} + + +
diff --git a/src/ui_ng/src/app/config/auth/config-auth.component.scss b/src/ui_ng/src/app/config/auth/config-auth.component.scss index e69de29bb..9634e8199 100644 --- a/src/ui_ng/src/app/config/auth/config-auth.component.scss +++ b/src/ui_ng/src/app/config/auth/config-auth.component.scss @@ -0,0 +1,3 @@ +clr-tooltip { + top: -1px; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/config/config.component.ts b/src/ui_ng/src/app/config/config.component.ts index 226795dc7..675c4c524 100644 --- a/src/ui_ng/src/app/config/config.component.ts +++ b/src/ui_ng/src/app/config/config.component.ts @@ -371,7 +371,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy { .catch(error => { this.testingLDAPOnGoing = false; let err = error._body; - if (!err) { + if (!err || !err.trim()) { err = 'UNKNOWN'; } this.msgHandler.showError('CONFIG.TEST_LDAP_FAILED', { 'param': err }); diff --git a/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.html b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.html new file mode 100644 index 000000000..d883b74bb --- /dev/null +++ b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.html @@ -0,0 +1,43 @@ + + + + + + + \ No newline at end of file diff --git a/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.scss b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.spec.ts b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.spec.ts new file mode 100644 index 000000000..a26876f21 --- /dev/null +++ b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddGroupModalComponent } from './add-group-modal.component'; + +describe('AddGroupModalComponent', () => { + let component: AddGroupModalComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AddGroupModalComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddGroupModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.ts b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.ts new file mode 100644 index 000000000..9a6fa9eae --- /dev/null +++ b/src/ui_ng/src/app/group/add-group-modal/add-group-modal.component.ts @@ -0,0 +1,109 @@ +import { Subscription } from 'rxjs/Subscription'; +import { Component, OnInit, EventEmitter, Output, ChangeDetectorRef, OnDestroy, ViewChild } from "@angular/core"; +import { NgForm } from "@angular/forms"; +import "rxjs/add/operator/finally"; + +import { GroupService } from "../group.service"; +import { MessageHandlerService } from "./../../shared/message-handler/message-handler.service"; +import { SessionService } from "./../../shared/session.service"; +import { UserGroup } from "./../group"; + +@Component({ + selector: "hbr-add-group-modal", + templateUrl: "./add-group-modal.component.html", + styleUrls: ["./add-group-modal.component.scss"] +}) +export class AddGroupModalComponent implements OnInit, OnDestroy { + opened = false; + mode = "create"; + dnTooltip = 'TOOLTIP.ITEM_REQUIRED'; + + group: UserGroup = new UserGroup(); + + formChangeSubscription: Subscription; + + @ViewChild('groupForm') + groupForm: NgForm; + + submitted = false; + + @Output() dataChange = new EventEmitter(); + + constructor( + private session: SessionService, + private msgHandler: MessageHandlerService, + private groupService: GroupService, + private cdr: ChangeDetectorRef + ) {} + + ngOnInit() { } + + + ngOnDestroy() { } + + public get isDNInvalid(): boolean { + let dnControl = this.groupForm.controls['ldap_group_dn']; + return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched); + } + + public get isFormValid(): boolean { + return this.groupForm.valid; + } + + public open(group?: UserGroup, editMode: boolean = false): void { + this.resetGroup(); + if (editMode) { + this.mode = "edit"; + Object.assign(this.group, group); + } else { + this.mode = "create"; + } + this.opened = true; + } + + public close(): void { + this.opened = false; + this.resetGroup(); + } + + save(): void { + if (this.mode === "create") { + this.createGroup(); + } else { + this.editGroup(); + } + } + + createGroup() { + let groupCopy = Object.assign({}, this.group); + this.groupService + .createGroup(groupCopy) + .finally(() => this.close()) + .subscribe( + res => { + this.msgHandler.showSuccess("GROUP.ADD_GROUP_SUCCESS"); + this.dataChange.emit(); + }, + error => this.msgHandler.handleError(error) + ); + } + + editGroup() { + let groupCopy = Object.assign({}, this.group); + this.groupService + .editGroup(groupCopy) + .finally(() => this.close()) + .subscribe( + res => { + this.msgHandler.showSuccess("ADD_GROUP_FAILURE"); + this.dataChange.emit(); + }, + error => this.msgHandler.handleError(error) + ); + } + + resetGroup() { + this.group = new UserGroup(); + this.groupForm.reset(); + } +} diff --git a/src/ui_ng/src/app/group/group.component.html b/src/ui_ng/src/app/group/group.component.html new file mode 100644 index 000000000..a2d3b313c --- /dev/null +++ b/src/ui_ng/src/app/group/group.component.html @@ -0,0 +1,40 @@ +
+
+

{{'GROUP.GROUP' | translate}}

+
+ + + + + +
+
+ + + + + + + + {{'GROUP.NAME' | translate}} + {{'GROUP.TYPE' | translate}} + {{'GROUP.DN' | translate}} + + + {{group.group_name}} + {{groupToSring(group.group_type) | translate}} + {{group.ldap_group_dn}} + + + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'USER.OF' | translate }} {{pagination.totalItems}} {{'GROUP.GROUPS' | translate}} + + + +
+ +
+
\ No newline at end of file diff --git a/src/ui_ng/src/app/group/group.component.scss b/src/ui_ng/src/app/group/group.component.scss new file mode 100644 index 000000000..0d3b56629 --- /dev/null +++ b/src/ui_ng/src/app/group/group.component.scss @@ -0,0 +1,45 @@ +.custom-add-button { + font-size: 12px; + margin-left: -12px; +} + +.filter-icon { + position: relative; + right: -12px; +} + +.filter-pos { + float: right; + margin-right: 24px; + position: relative; + top: 10px; +} + +.action-panel-pos { + position: relative; + padding-left: 12px; + margin-top: 12px; +} + +.refresh-btn { + position: absolute; + right: 6px; + top: 17px; + cursor: pointer; +} + +.refresh-btn:hover { + color: #007CBB; +} + +.hide-create { + visibility: hidden !important; +} + +.rightPos { + position: absolute; + right: 20px; + margin-top: -7px; + height: 32px; + z-index: 100; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/group/group.component.spec.ts b/src/ui_ng/src/app/group/group.component.spec.ts new file mode 100644 index 000000000..50601d077 --- /dev/null +++ b/src/ui_ng/src/app/group/group.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GroupComponent } from './group.component'; + +describe('GroupComponent', () => { + let component: GroupComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GroupComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ui_ng/src/app/group/group.component.ts b/src/ui_ng/src/app/group/group.component.ts new file mode 100644 index 000000000..6dbd6949d --- /dev/null +++ b/src/ui_ng/src/app/group/group.component.ts @@ -0,0 +1,166 @@ +import { SessionService } from "./../shared/session.service"; +import { TranslateService } from "@ngx-translate/core"; +import { Observable } from "rxjs/Observable"; +import { Subscription } from "rxjs/Subscription"; +import { Component, OnInit, ViewChild, OnDestroy } from "@angular/core"; +import {operateChanges, OperateInfo, OperationService, OperationState} from "harbor-ui"; + +import { + ConfirmationTargets, + ConfirmationState, + ConfirmationButtons +} from "../shared/shared.const"; +import { ConfirmationMessage } from "../shared/confirmation-dialog/confirmation-message"; +import { ConfirmationDialogService } from "./../shared/confirmation-dialog/confirmation-dialog.service"; +import { AddGroupModalComponent } from "./add-group-modal/add-group-modal.component"; +import { UserGroup } from "./group"; +import { GroupService } from "./group.service"; +import { MessageHandlerService } from "../shared/message-handler/message-handler.service"; + +@Component({ + selector: "app-group", + templateUrl: "./group.component.html", + styleUrls: ["./group.component.scss"] +}) +export class GroupComponent implements OnInit, OnDestroy { + searchTerm = ""; + loading = true; + groups: UserGroup[] = []; + currentPage = 1; + totalCount = 0; + selectedGroups: UserGroup[] = []; + currentTerm = ""; + delSub: Subscription; + batchOps = 'idle'; + batchInfos = new Map(); + + @ViewChild(AddGroupModalComponent) newGroupModal: AddGroupModalComponent; + + constructor( + private operationService: OperationService, + private translate: TranslateService, + private operateDialogService: ConfirmationDialogService, + private groupService: GroupService, + private msgHandler: MessageHandlerService, + private session: SessionService + ) {} + + ngOnInit() { + this.loadData(); + this.delSub = this.operateDialogService.confirmationConfirm$.subscribe( + message => { + if ( + message && + message.state === ConfirmationState.CONFIRMED && + message.source === ConfirmationTargets.PROJECT_MEMBER + ) { + if (this.batchOps === 'delete') { + this.deleteGroups(); + } + } + } + ); + } + ngOnDestroy(): void { + this.delSub.unsubscribe(); + } + + refresh(): void { + this.loadData(); + } + + loadData(): void { + this.loading = true; + this.groupService.getUserGroups().subscribe(groups => { + this.groups = groups.filter(group => { + if (!group.group_name) {group.group_name = ''; } + return group.group_name.includes(this.searchTerm); + } + ); + this.loading = false; + }); + } + + addGroup(): void { + this.newGroupModal.open(); + } + + editGroup(): void { + this.newGroupModal.open(this.selectedGroups[0], true); + } + + openDeleteConfirmationDialog(): void { + // open delete modal + this.batchOps = 'delete'; + let nameArr: string[] = []; + if (this.selectedGroups.length > 0) { + this.selectedGroups.forEach(group => { + nameArr.push(group.group_name); + }); + // batchInfo.id = group.id; + let deletionMessage = new ConfirmationMessage( + "MEMBER.DELETION_TITLE", + "MEMBER.DELETION_SUMMARY", + nameArr.join(","), + this.selectedGroups, + ConfirmationTargets.PROJECT_MEMBER, + ConfirmationButtons.DELETE_CANCEL + ); + this.operateDialogService.openComfirmDialog(deletionMessage); + } + } + + deleteGroups() { + let obs = this.selectedGroups.map(group => { + let operMessage = new OperateInfo(); + operMessage.name = 'OPERATION.DELETE_GROUP'; + operMessage.data.id = group.id; + operMessage.state = OperationState.progressing; + operMessage.data.name = group.group_name; + + this.operationService.publishInfo(operMessage); + return this.groupService + .deleteGroup(group.id) + .flatMap(response => { + return this.translate.get("BATCH.DELETED_SUCCESS").flatMap(res => { + operateChanges(operMessage, OperationState.success); + return Observable.of(res); + }); + }) + .catch(err => { + return this.translate.get("BATCH.DELETED_FAILURE").flatMap(res => { + operateChanges(operMessage, OperationState.failure, res); + return Observable.of(res); + }); + }); + }); + + Observable.forkJoin(obs).subscribe( + res => { + this.selectedGroups = []; + this.batchOps = 'idle'; + this.loadData(); + }, + err => this.msgHandler.handleError(err) + ); + } + + groupToSring(type: number) { + if (type === 1) {return 'GROUP.LDAP_TYPE'; } else {return 'UNKNOWN'; } + } + + doFilter(groupName: string): void { + this.searchTerm = groupName; + this.loadData(); + } + get canAddGroup(): boolean { + return this.session.currentUser.has_admin_role; + } + + get canEditGroup(): boolean { + return ( + this.selectedGroups.length === 1 && + this.session.currentUser.has_admin_role + ); + } +} diff --git a/src/ui_ng/src/app/group/group.module.ts b/src/ui_ng/src/app/group/group.module.ts new file mode 100644 index 000000000..45464c1e7 --- /dev/null +++ b/src/ui_ng/src/app/group/group.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { SharedModule } from '../shared/shared.module'; +import { GroupComponent } from './group.component'; +import { AddGroupModalComponent } from './add-group-modal/add-group-modal.component'; +import { GroupService } from './group.service'; + + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + FormsModule, + ReactiveFormsModule + ], + exports: [ + GroupComponent, + AddGroupModalComponent, + FormsModule, + ReactiveFormsModule + ], + providers: [ GroupService ], + declarations: [GroupComponent, AddGroupModalComponent] +}) +export class GroupModule { } diff --git a/src/ui_ng/src/app/group/group.service.spec.ts b/src/ui_ng/src/app/group/group.service.spec.ts new file mode 100644 index 000000000..21236835b --- /dev/null +++ b/src/ui_ng/src/app/group/group.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { GroupService } from './group.service'; + +describe('GroupService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [GroupService] + }); + }); + + it('should be created', inject([GroupService], (service: GroupService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/ui_ng/src/app/group/group.service.ts b/src/ui_ng/src/app/group/group.service.ts new file mode 100644 index 000000000..290f03bc4 --- /dev/null +++ b/src/ui_ng/src/app/group/group.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from "@angular/core"; +import { Http, Response } from "@angular/http"; +import { Observable } from "rxjs/Observable"; +import "rxjs/add/observable/of"; +import "rxjs/add/operator/delay"; +import "rxjs/add/operator/toPromise"; + +import { UserGroup } from "./group"; +import { HTTP_JSON_OPTIONS, HTTP_GET_OPTIONS } from "../shared/shared.utils"; + +const userGroupEndpoint = "/api/usergroups"; +const ldapGroupSearchEndpoint = "/api/ldap/groups/search?groupname="; + +@Injectable() +export class GroupService { + constructor(private http: Http) {} + + private extractData(res: Response) { + if (res.text() === '') {return []; }; + return res.json() || []; + } + private handleErrorObservable(error: Response | any) { + console.error(error.message || error); + return Observable.throw(error.message || error); + } + + getUserGroups(): Observable { + return this.http.get(userGroupEndpoint, HTTP_GET_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(error => { + return this.handleErrorObservable(error); + }); + } + + createGroup(group: UserGroup): Observable { + return this.http + .post(userGroupEndpoint, group, HTTP_JSON_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } + + getGroup(group_id: number): Observable { + return this.http + .get(`${userGroupEndpoint}/${group_id}`, HTTP_JSON_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } + + editGroup(group: UserGroup): Observable { + return this.http + .put(`${userGroupEndpoint}/${group.id}`, group, HTTP_JSON_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } + + deleteGroup(group_id: number): Observable { + return this.http + .delete(`${userGroupEndpoint}/${group_id}`) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } + + searchGroup(group_name: string): Observable { + return this.http + .get(`${ldapGroupSearchEndpoint}${group_name}`, HTTP_GET_OPTIONS) + .map(response => { + return this.extractData(response); + }) + .catch(this.handleErrorObservable); + } +} diff --git a/src/ui_ng/src/app/group/group.ts b/src/ui_ng/src/app/group/group.ts new file mode 100644 index 000000000..855681162 --- /dev/null +++ b/src/ui_ng/src/app/group/group.ts @@ -0,0 +1,12 @@ +export class UserGroup { + id?: number; + group_name?: string; + group_type: number; + ldap_group_dn?: string; + + constructor() { + { + this.group_type = 1; + } + } +} diff --git a/src/ui_ng/src/app/harbor-routing.module.ts b/src/ui_ng/src/app/harbor-routing.module.ts index 0de5f5d00..3e52fa267 100644 --- a/src/ui_ng/src/app/harbor-routing.module.ts +++ b/src/ui_ng/src/app/harbor-routing.module.ts @@ -27,6 +27,7 @@ import { ConfigurationComponent } from './config/config.component'; import { UserComponent } from './user/user.component'; import { SignInComponent } from './account/sign-in/sign-in.component'; import { ResetPasswordComponent } from './account/password-setting/reset-password/reset-password.component'; +import { GroupComponent } from './group/group.component'; import { TotalReplicationPageComponent } from './replication/total-replication/total-replication-page.component'; import { DestinationPageComponent } from './replication/destination/destination-page.component'; @@ -74,6 +75,11 @@ const harborRoutes: Routes = [ component: UserComponent, canActivate: [SystemAdminGuard] }, + { + path: 'groups', + component: GroupComponent, + canActivate: [SystemAdminGuard] + }, { path: 'registries', component: DestinationPageComponent, diff --git a/src/ui_ng/src/app/project/member/add-group/add-group.component.html b/src/ui_ng/src/app/project/member/add-group/add-group.component.html new file mode 100644 index 000000000..3c1c0cfaf --- /dev/null +++ b/src/ui_ng/src/app/project/member/add-group/add-group.component.html @@ -0,0 +1,104 @@ + + + + + + \ No newline at end of file diff --git a/src/ui_ng/src/app/project/member/add-group/add-group.component.scss b/src/ui_ng/src/app/project/member/add-group/add-group.component.scss new file mode 100644 index 000000000..1d9d5d98b --- /dev/null +++ b/src/ui_ng/src/app/project/member/add-group/add-group.component.scss @@ -0,0 +1,18 @@ +clr-datagrid { + ::ng-deep .datagrid { + margin-top: 0; + } +} + +.row { + margin-top: 12px; +} + +.modeSelectradios { + margin-top: 21px; +} +.filterTool { + position: relative; + z-index: 100; + right: 15px; +} \ No newline at end of file diff --git a/src/ui_ng/src/app/project/member/add-group/add-group.component.spec.ts b/src/ui_ng/src/app/project/member/add-group/add-group.component.spec.ts new file mode 100644 index 000000000..e78989208 --- /dev/null +++ b/src/ui_ng/src/app/project/member/add-group/add-group.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddGroupComponent } from './add-group.component'; + +describe('AddGroupComponent', () => { + let component: AddGroupComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AddGroupComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddGroupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ui_ng/src/app/project/member/add-group/add-group.component.ts b/src/ui_ng/src/app/project/member/add-group/add-group.component.ts new file mode 100644 index 000000000..b5ea17a16 --- /dev/null +++ b/src/ui_ng/src/app/project/member/add-group/add-group.component.ts @@ -0,0 +1,167 @@ +import { ChangeDetectorRef, ChangeDetectionStrategy, ViewChild } from "@angular/core"; +import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; +import { NgForm } from '@angular/forms'; + +import { forkJoin } from "rxjs/observable/forkJoin"; +import { Observable } from "rxjs/Observable"; +import "rxjs/observable/of"; +import { TranslateService } from '@ngx-translate/core'; + +import "rxjs/observable/timer"; +import {operateChanges, OperateInfo, OperationService, OperationState} from "harbor-ui"; + +import { UserGroup } from "./../../../group/group"; +import { MemberService } from "./../member.service"; +import { GroupService } from "../../../group/group.service"; +import { ProjectRoles } from "../../../shared/shared.const"; +import { MessageHandlerService } from '../../../shared/message-handler/message-handler.service'; +import { Member } from "../member"; + +@Component({ + selector: "add-group", + templateUrl: "./add-group.component.html", + styleUrls: ["./add-group.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AddGroupComponent implements OnInit { + opened = false; + createGroupMode = false; + onLoading = false; + roles = ProjectRoles; + currentTerm = ''; + + selectedRole = 1; + group = new UserGroup(); + selectedGroups: UserGroup[] = []; + groups: UserGroup[] = []; + + dnTooltip = 'TOOLTIP.ITEM_REQUIRED'; + + @Input() projectId: number; + @Input() memberList: Member[] = []; + @Output() added = new EventEmitter(); + + @ViewChild('groupForm') + groupForm: NgForm; + + constructor( + private translateService: TranslateService, + private msgHandler: MessageHandlerService, + private operationService: OperationService, + private ref: ChangeDetectorRef, + private groupService: GroupService, + private memberService: MemberService + ) {} + + ngOnInit() { } + + public get isValid(): boolean { + if (this.createGroupMode) { + return this.groupForm && this.groupForm.valid; + } else { + return this.selectedGroups.length > 0; + } + } + public get isDNInvalid(): boolean { + if (!this.groupForm) {return false; }; + let dnControl = this.groupForm.controls['ldap_group_dn']; + return dnControl && dnControl.invalid && (dnControl.dirty || dnControl.touched); + } + + loadGroups() { + this.onLoading = true; + this.groupService.getUserGroups().subscribe(groups => { + this.groups = groups.filter(group => { + if (!group.group_name) {group.group_name = ''; }; + return group.group_name.includes(this.currentTerm) + && !this.memberList.some(member => member.entity_type === 'g' && member.entity_id === group.id); + }); + this.onLoading = false; + this.ref.detectChanges(); + }); + } + + doFilter(name: string) { + this.currentTerm = name; + this.loadGroups(); + } + + resetModaldata() { + this.group = new UserGroup(); + this.selectedRole = 1; + this.selectedGroups = []; + this.groups = []; + } + + public open() { + this.resetModaldata(); + this.loadGroups(); + this.opened = true; + this.ref.detectChanges(); + } + + public close() { + this.resetModaldata(); + this.opened = false; + } + + onSave() { + if (!this.createGroupMode) { + this.addGroups(); + } else { + this.createGroupAsMember(); + } + } + + onCancel() { + this.opened = false; + } + + addGroups() { + let GroupAdders$ = this.selectedGroups.map(group => { + let operMessage = new OperateInfo(); + operMessage.name = 'OPERATION.ADD_GROUP'; + operMessage.data.id = group.id; + operMessage.state = OperationState.progressing; + operMessage.data.name = group.group_name; + this.operationService.publishInfo(operMessage); + return this.memberService + .addGroupMember(this.projectId, group, this.selectedRole) + .flatMap(response => { + return this.translateService.get("BATCH.DELETED_SUCCESS") + .flatMap(res => { + operateChanges(operMessage, OperationState.success); + return Observable.of(res); + }); }) + .catch(error => { + return this.translateService.get("BATCH.DELETED_FAILURE") + .flatMap(res => { + operateChanges(operMessage, OperationState.failure, res); + return Observable.of(res); + }); }) + .catch(error => Observable.of(error.status)); + }); + forkJoin(GroupAdders$) + .subscribe(results => { + if (results.some(code => code < 200 || code > 299)) { + this.added.emit(false); + } else { + this.added.emit(true); + } + }); + this.opened = false; + } + + createGroupAsMember() { + let groupCopy = Object.assign({}, this.group); + this.memberService.addGroupMember(this.projectId, groupCopy, this.selectedRole) + .subscribe( + res => this.added.emit(true), + err => { + this.msgHandler.handleError(err); + this.added.emit(false); + } + ); + this.opened = false; + } +} diff --git a/src/ui_ng/src/app/project/member/add-member/add-member.component.html b/src/ui_ng/src/app/project/member/add-member/add-member.component.html index 6cb1cdac9..309d289f3 100644 --- a/src/ui_ng/src/app/project/member/add-member/add-member.component.html +++ b/src/ui_ng/src/app/project/member/add-member/add-member.component.html @@ -1,18 +1,19 @@ - - +