企业级镜像管理系统Harbor

以下内容转载于Breeze
原博客地址:https://www.cnblogs.com/breezey/

十三、企业级镜像管理系统Harbor

1、Harbor简介

在说harbor之前,我们首先说一说直接使用docker registry的一些缺陷:

  1. 缺少认证机制,任何人都可以随意拉取及上传镜像,安全性缺失
  2. 缺乏镜像清理机制,镜像可以push却不能删除,日积月累,占用空间会越来越大
  3. 缺乏相应的扩展机制

鉴于以上缺点,我们通常在生产环境中,不会直接使用docker registry来实现提供镜像服务。而Harbor正好解决了上述所有的问题。

Harbor是一个用于存储和分发Docker镜像的企业级Registry服务器,通过添加一些企业必需的功能特性,例如安全、标识和管理等,扩展了开源Docker Distribution。作为一个企业级私有Registry服务器,Harbor提供了更好的性能和安全。提升用户使用Registry构建和运行环境传输镜像的效率。Harbor支持安装在多个Registry节点的镜像资源复制,镜像全部保存在私有Registry中,确保数据和知识产权在公司内部网络中管控。另外,Harbor也提供了高级的安全特性,诸如用户管理,访问控制和活动审计等。

Harbor官方网站:http://vmware.github.io/harbor/

Harbor源码地址:https://github.com/vmware/harbor

harbor的二进制包同时提供online和offline版本,我们这里直接使用online版本。

2、配置

2.1、架构图

u=2756639760,2037643405&fm=26&gp=0.jpg

如上图所示,harbor由6大模块级成:

  • Proxy: Harbor的registry、UI、token services等组件,都处在一个反向代理后边。该代理将来自浏览器、docker clients的请求转发到后端服务上。
  • Registry: 负责存储Docker镜像,以及处理Docker push/pull请求。因为Harbor强制要求对镜像的访问做权限控制, 在每一次push/pull请求时,Registry会强制要求客户端从token service那里获得一个有效的token。
  • Core services: Harbor的核心功能,主要包括如下3个服务:
    • UI: 作为Registry Webhook, 以图像用户界面的方式辅助用户管理镜像。1) WebHook是在registry中配置的一种机制, 当registry中镜像发生改变时,就可以通知到Harbor的webhook endpoint。Harbor使用webhook来更新日志、初始化同步job等。 2) Token service会根据该用户在一个工程中的角色,为每一次的push/pull请求分配对应的token。假如相应的请求并没有包含token的话,registry会将该请求重定向到token service。 3) Database 用于存放工程元数据、用户数据、角色数据、同步策略以及镜像元数据。
    • Job services: 主要用于镜像复制,本地镜像可以被同步到远程Harbor实例上。
    • Log collector: 负责收集其他模块的日志到一个地方

2.2、组件说明

需要说明的是,harbor的每个组件都是以Docker容器的形式构建的,可以使用Docker Compose来进行部署,当然,如果你的环境中使用了kubernetes,harbor也提供了kubernetes的配置文件。

harbor共有8个容器组成:

  • ui:harbor的核心服务。
  • log:运行着rsyslog的容器,进行日志收集。
  • mysql:由官方mysql镜像构成的数据库容器
  • nginx:使用Nginx做反向代理
  • registry:官方的Docker registry
  • adminserver:harbor的配置数据管理器
  • jobservice:Harbor的任务管理服务。
  • redis:用于存储session

下面我们所有的配置都以harbor 1.5.2版本作配置说明。

harbor可以支持http和https,建议使用https,https证书最好是受信任的ca颁发的证书,这样,在配置docker的时候,就不需要添加"insecure-registries"配置项。我们这里就直接使用自签名证书。

生成证书:

openssl genrsa -des3 -out server.key 1024
openssl rsa -in server.key -out server.key    #去除server.key的加密口令
openssl req -new -key server.key -out server.csr
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

将证书放入/data/certs/目录下:

cp server.crt server.key /data/certs

2.3、harbor.cfg配置文件示例

harbor的核心配置文件是harbor.cfg,路径在源码目录下的make目录下,示例配置如下:

_version = 1.5.0

# harbor的访问地址
hostname = hub.dz11.com

# 使用https协议
ui_url_protocol = https

# harbor的数据库密码
db_password = xxxxx

max_job_workers = 50
customize_crt = on

# 证书相关路径
ssl_cert = /data/certs/dz11.com.crt
ssl_cert_key = /data/certs/dz11.com.key
secretkey_path = /data

admiral_url = NA

# 定义日志滚动
log_rotate_count = 50
log_rotate_size = 200M

http_proxy =
https_proxy =
no_proxy = 127.0.0.1,localhost,ui

# email相关配置
email_identity = 
email_server = smtp.163.com
email_server_port = 25
email_username = [email protected]
email_password = yan_ruo_gu0711
email_from = admin 
email_ssl = false

# 登录harbor的管理员密码
harbor_admin_password = xxxxxx

# harbor的验证方式,支持db_auth和ldap_auth,这里使用的是ldap_auth,如果使用db_auth的话,直接将auth_mode修改为db_auth即可,ldap的配置不再需要。
auth_mode = ldap_auth
ldap_url = ldap://10.1.1.1:389
ldap_searchdn = CN=jenkins,OU=LDAP,OU=Dev-wh.xxx.com,DC=dev-wh,DC=xxx,DC=com
ldap_search_pwd = xxxx
ldap_basedn = OU=Dev-wh.xxx.com,DC=dev-wh,DC=xxx,DC=com
ldap_filter = (objectClass=person)
ldap_uid = 0
ldap_scope = 2 
ldap_timeout = 5

self_registration = on
token_expiration = 30
project_creation_restriction = everyone
verify_remote_cert = on

# 数据库相关配置,默认如果不需要使用自建的数据库,这些配置就都不需要
db_host = mysql
db_password = root123
db_port = 3306
db_user = root
redis_url = redis:6379
clair_db_host = postgres
clair_db_password = password
clair_db_port = 5432
clair_db_username = postgres
clair_db = postgres
uaa_endpoint = uaa.mydomain.org
uaa_clientid = id
uaa_clientsecret = secret
uaa_verify_cert = true
uaa_ca_cert = /path/to/ca.pem
registry_storage_provider_name = filesystem
registry_storage_provider_config =

2.4、部署harbor

harbor支持docker-compose和kubernetes的部署方式,默认采用docker-compose作单机部署。

先执行./prepare,然后执行./install.sh进行启动。执行./install.sh的时候,即调用了docker-compose运行了当前目录下的docker-compose.yml文件。

在执行.prepare的时候抛出如下异常:

root@ubuntu:~/harbor# ./prepare 
Generated and saved secret to file: /data/secretkey
Generated configuration file: ./common/config/nginx/nginx.conf
Generated configuration file: ./common/config/adminserver/env
Generated configuration file: ./common/config/ui/env
Generated configuration file: ./common/config/registry/config.yml
Generated configuration file: ./common/config/db/env
Generated configuration file: ./common/config/jobservice/env
Generated configuration file: ./common/config/jobservice/config.yml
Generated configuration file: ./common/config/log/logrotate.conf
Generated configuration file: ./common/config/jobservice/config.yml
Generated configuration file: ./common/config/ui/app.conf
Fail to generate key file: ./common/config/ui/private_key.pem, cert file: ./common/config/registry/root.crt

需要修改prepare文件,将第498行:

empty_subj = "/C=/ST=/L=/O=/CN=/"

修改如下:

empty_subj = "/C=US/ST=California/L=Palo Alto/O=VMware, Inc./OU=Harbor/CN=notarysigner"

在实际启动过程中,出现过registry启动失败的情况,/var/log/harbor/registry.log输出如下:

May 30 21:06:00 172.18.0.1 registry[3218]: panic: unable to configure authorization (token): unable to open token auth root certificate bundle file "/etc/registry/root.crt": open /etc/registry/root.crt: permission denied
May 30 21:06:00 172.18.0.1 registry[3218]: 
May 30 21:06:00 172.18.0.1 registry[3218]: goroutine 1 [running]:
May 30 21:06:00 172.18.0.1 registry[3218]: panic(0xb4cd40, 0xc4203ae160)
May 30 21:06:00 172.18.0.1 registry[3218]: #011/usr/local/go/src/runtime/panic.go:500 +0x1a1
May 30 21:06:00 172.18.0.1 registry[3218]: github.com/docker/distribution/registry/handlers.NewApp(0x1067820, 0xc4203a8630, 0xc4202df180, 0x1067820)
May 30 21:06:00 172.18.0.1 registry[3218]: #011/go/src/github.com/docker/distribution/registry/handlers/app.go:302 +0x1b6a
May 30 21:06:00 172.18.0.1 registry[3218]: github.com/docker/distribution/registry.NewRegistry(0x7fcfa30dd198, 0xc4203a8630, 0xc4202df180, 0xe, 0x0, 0x0)
May 30 21:06:00 172.18.0.1 registry[3218]: #011/go/src/github.com/docker/distribution/registry/registry.go:86 +0x213
May 30 21:06:00 172.18.0.1 registry[3218]: github.com/docker/distribution/registry.glob..func1(0x108f1a0, 0xc42036d240, 0x1, 0x1)
May 30 21:06:00 172.18.0.1 registry[3218]: #011/go/src/github.com/docker/distribution/registry/registry.go:55 +0x106
May 30 21:06:00 172.18.0.1 registry[3218]: github.com/docker/distribution/vendor/github.com/spf13/cobra.(*Command).execute(0x108f1a0, 0xc42036d1f0, 0x1, 0x1, 0x108f1a0, 0xc42036d1f0)
May 30 21:06:00 172.18.0.1 registry[3218]: #011/go/src/github.com/docker/distribution/vendor/github.com/spf13/cobra/command.go:495 +0x190
May 30 21:06:00 172.18.0.1 registry[3218]: github.com/docker/distribution/vendor/github.com/spf13/cobra.(*Command).Execute(0x108f340, 0xc4201d7f40, 0xc4200001a0)
May 30 21:06:00 172.18.0.1 registry[3218]: #011/go/src/github.com/docker/distribution/vendor/github.com/spf13/cobra/command.go:560 +0x3c3
May 30 21:06:00 172.18.0.1 registry[3218]: main.main()
May 30 21:06:00 172.18.0.1 registry[3218]: #011/go/src/github.com/docker/distribution/cmd/registry/main.go:24 +0x2d

这是因为registry容器没有/etc/registry/root.crt的访问权限导致。这个文件默认是挂载的./common/config/registry/root.crt这个文件,所以我们需要对这个文件作授权。

通过观察可知,harbor的容器启动用户非root身份,而是以一个uid和gid都为10000的用户。所以只需要为该用户授权即可:

chown 10000.10000 ./common/config/registry/root.crtsh

正常启动之后,配置完成。

可以通过如下方式访问: https://hub.dz11.com

2.5、注意事项

需要说明的是,harbor支持http和https,但如果使用http的话,在拉取镜像的时候,会抛出仓库不受信任的异常。需要在所有的docker客户端的docker配置文件/etc/docker/daemon.json中添加如下配置:

{
    "insecure-registries": ["https://hub.dz11.com"],
}

如果使用自签名的https证书,仍然会提示证书不受信任的问题。需要将自签名的ca证书发送到所有的docker客户端的指定目录。

关于使用自签名证书配置harbor的具体过程可以参考:https://github.com/WingkaiHo/docker-calico/blob/master/harbor/README.md

3、Harbor高可用配置

3.1、双主复制

3.1.1、主从同步

harbor官方默认提供主从复制的方案来解决镜像同步问题,通过复制的方式,我们可以实时将测试环境harbor仓库的镜像同步到生产环境harbor,类似于如下流程:

[图片上传失败...(image-aa9efe-1609833558998)]

在实际生产运维的中,往往需要把镜像发布到几十或上百台集群节点上。这时,单个Registry已经无法满足大量节点的下载需求,因此要配置多个Registry实例做负载均衡。手工维护多个Registry实例上的镜像,将是十分繁琐的事情。Harbor可以支持一主多从的镜像发布模式,可以解决大规模镜像发布的难题:

[图片上传失败...(image-f11a70-1609833558998)]

只要往一台Registry上发布,镜像就像“仙女散花”般地同步到多个Registry中,高效可靠。

如果是地域分布较广的集群,还可以采用层次型发布方式,如从集团总部同步到省公司,从省公司再同步到市公司:

[图片上传失败...(image-b16a0d-1609833558998)]

然而单靠主从同步,仍然解决不了harbor主节点的单点问题。

3.1.2、双主复制说明

所谓的双主复制其实就是复用主从同步实现两个harbor节点之间的双向同步,来保证数据的一致性,然后在两台harbor前端顶一个负载均衡器将进来的请求分流到不同的实例中去,只要有一个实例中有了新的镜像,就是自动的同步复制到另外的的实例中去,这样实现了负载均衡,也避免了单点故障,在一定程度上实现了Harbor的高可用性:

[图片上传失败...(image-201a3a-1609833558998)]

这个方案有一个问题就是有可能两个Harbor实例中的数据不一致。假设如果一个实例A挂掉了,这个时候有新的镜像进来,那么新的镜像就会在另外一个实例B中,后面即使恢复了挂掉的A实例,Harbor实例B也不会自动去同步镜像,这样只能手动的先关掉Harbor实例B的复制策略,然后再开启复制策略,才能让实例B数据同步,让两个实例的数据一致。

另外,我还需要多吐槽一句,在实际生产使用中,主从复制十分的不靠谱。

所以这里推荐使用下面要说的这种方案。

3.2、多harbor实例共享后端存储

3.2.1、方案说明

共享后端存储算是一种比较标准的方案,就是多个Harbor实例共享同一个后端存储,任何一个实例持久化到存储的镜像,都可被其他实例中读取。通过前置LB进来的请求,可以分流到不同的实例中去处理,这样就实现了负载均衡,也避免了单点故障:

[图片上传失败...(image-be47d8-1609833558998)]

这个方案在实际生产环境中部署需要考虑三个问题:

  1. 共享存储的选取,Harbor的后端存储目前支持AWS S3、Openstack Swift, Ceph等,在我们的实验环境里,就直接使用nfs
  2. Session在不同的实例上共享,这个现在其实已经不是问题了,在最新的harbor中,默认session会存放在redis中,我们只需要将redis独立出来即可。可以通过redis sentinel或者redis cluster等方式来保证redis的可用性。在我们的实验环境里,仍然使用单台redis
  3. Harbor多实例数据库问题,这个也只需要将harbor中的数据库拆出来独立部署即可。让多实例共用一个外部数据库,数据库的高可用也可以通过数据库的高可用方案保证。

3.2.2、环境说明

实验环境:

ip role
192.168.198.133 harbor
192.168.198.135 harbor
192.168.198.136 redis、mysql、nfs

需要强调的是,我们的环境中,不包括负载均衡器的配置,请自行查阅负载均衡配置相关文档

3.3.3、配置说明

安装nfs
# 安装nfs
apt install nfs-kernel-server nfs-common

# 编辑/etc/exports文件
/data   *(rw,no_root_squash)

chmod 777 -R /data

systemctl start nfs-server
安装redis和mysql

这里我们就直接通过docker安装,docker-compose.yml文件内容如下:

version: '3'
services:
  mysql-server:
    hostname: mysql-server
    container_name: mysql-server
    image: mysql:5.7
    network_mode: host
    volumes:
      - /mysql57/data:/var/lib/mysql
    command: --character-set-server=utf8
    environment:
      MYSQL_ROOT_PASSWORD: 123456
  redis:
    hostname: redis-server
    container_name: redis-server
    image: redis:3
    network_mode: host

启动:

docker-compose up -d
导入registry数据库

配置好了mysql以后,还需要往mysql数据库中导入harbor registry库。在《企业级镜像管理系统》中,我们安装了一个单机版harbor,启动了一个mysql,里面有一个registry数据库,直接导出来,然后再导入到新数据库中:

# 导出数据库:

docker exec -it harbor_db /bin/bash
mysqldump -uroot -p --databases registry > registry.dump

# 在宿主机上将registry.dump复制出来
docker cp  harbor_db:/registry.dump ./

# 将宿主机上的registry.dump复制到独立的mysql容器中

docker cp ./registry.dump :/registry.dump

# 在独立的mysql容器将将registry数据库导入

docker exec -it  /bin/bash

mysql -uroot -p

mysql> source /registry.dump

3.3、配置harbor

挂载nfs目录

在harbor节点上挂载nfs目录:

mount -t nfs 192.168.198.136:/data /data

修改harbor.cfg配置

在harbor节点上,下载好harbor的安装包,生成好自签名证书,修改prepare文件,可直接参考《企业级镜像管理系统Harbor》,不同的是,harbor.cfg文件需要修改数据库及redis配置如下:

db_host = 192.168.198.136
db_password = 123456
db_port = 3306
db_user = root
redis_url = 192.168.198.136:6379

修改docker-compose.yml配置

与单机版harbor相比,集群配置不再需要启动mysql和redis,所以docker-compose.yml也需要作相应修改。事实上,在harbor的安装目录中,有个ha的目录,里面已经提供了我们需要的docker-compose.yml文件,只需要复制出来即可。实际上,在这个目录中,还提供了使用lvs作为负载均衡器时,keepalived的配置。

cp ha/docker-compose.yml
./prepare
./install.sh

在两个harbor节点上完成安装以后,我们可以通过绑定hosts到不同的节点来验证两个节点的负载均衡效果。

4、Harbor之Swagger REST API

4.1、Swagger介绍

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTFul 风格的 Web 服务。通过 Swagger,我们可以方便的、快速的实现 RESTFul API,同时它也提供UI界面,可以直观的管理和测试各个API接口,它还可以集成到各种开发语言中,大大提高了我们日常工作效率。

Harbor也提供这样一个东东,不过默认没有安装。

详细的安装说明,可以直接参考官方文档:https://github.com/goharbor/harbor/blob/master/docs/configure_swagger.md

我这里仅做一些简要配置说明。

4.2、Harbor Swagger预览

Harbor提供一种通过在线Swagger编辑器预览Harbor REST API信息的方式。在线编辑器的地址:http://editor.swagger.io/

不过使用在线Swagger编辑器,需要我们提供harbor的swagger.yaml文件,下面是这个文件的两个下载地址:

URL1:https://raw.githubusercontent.com/vmware/harbor/master/docs/swagger.yaml
URL2:https://github.com/vmware/harbor/blob/master/docs/swagger.yaml

只需要将文件内容直接粘贴到swagger在线编辑器的左边即可。或者在swagger编辑器页面通过File --> Importfile导入yaml。

4.3、整合本地Harbor与Swagger

当然,最好的方式,是直接将Swagger与我们自建的harbor服务整合,这样可以实现在线的接口测试。

1. 下载prepare-swagger.sh以及swagger.yaml到你本地的harbor目录,我这里就直接放到了/usr/local/harbor中:

wget https://raw.githubusercontent.com/goharbor/harbor/master/docs/prepare-swagger.sh https://raw.githubusercontent.com/goharbor/harbor/master/docs/swagger.yaml

2. 修改prepre-swagger.sh

# 指定harbor的协议,如https/http
SCHEME=
# 指定harbor的访问地址,如hub.test.com
SERVER_IP=

3. 执行prepare-swagger.sh

chmod +x ./prepare-swagger.sh
./prepare-swagger.sh

4. 修改harbor的docker-compose.yml文件,如下:

...
ui:
  ... 
  volumes:
    - ./common/config/ui/app.conf:/etc/core/app.conf:z
    - ./common/config/ui/private_key.pem:/etc/core/private_key.pem:z
    - /data/secretkey:/etc/core/key:z
    - /data/ca_download/:/etc/core/ca/:z
    ## add two lines as below ##
    - ../src/ui/static/vendors/swagger-ui-2.1.4/dist/:/harbor/static/vendors/swagger/:z
    - ../src/ui/static/resources/yaml/swagger.yaml:/harbor/static/resources/yaml/swagger.yaml
    ...
    

5. 重建harbor容器

 docker-compose down -v && docker-compose up -d

6. 访问

https:///static/vendors/swagger/index.html

在使用 Harbor API 时,是需要 session ID 的,当我们未登录 Harbor 时,直接使用API将得不到任何结果,这里如果我们想使用 Swagger UI 点击访问API返回结果,那么需要在浏览器中先登录 Harbor Web UI,登录后新开一个tab,在这个tab访问 Harbor Swagger Web,将会得到正常的响应结果,因为这时session已经共享,会话认证通过

5、Harbor镜像清理

docker镜像仓库中镜像的清理,一直是个比较麻烦的事情。尤其是在测试环境当中,每天都会有大量的构建。由此会产生大量的历史镜像,而这些镜像,大多数都没有用。

在harbor中,清理镜像,也得分为两步,第一步是从ui中删除历史镜像。这个时候镜像并不会被真正删除,好在harbor集成了镜像删除的功能。

5.1、清理UI中的镜像

清理ui中的镜像,如果直接通过图形界面一个个的点击删除的话,在有大规模镜像需要清理的时候,简直就是灾难,而且这种方式,实在太low。

我这里简单写了个脚本,以实现如下功能:

  1. 遍历所有project
  2. 获取project中所有tag数超过30的repositories
  3. 获取这些tag数超过30的repositories的所有tag
  4. 基于时间排序,保留最新的30个tag
  5. 删除其他tag

脚本示例:

#! /usr/bin/env python
# -*- coding:utf-8 -*-


import requests
import json


class RequestClient(object):

    def __init__(self,login_url, username, password):
        self.username = username
        self.password =  password
        self.login_url = login_url
        self.session = requests.Session()
        self.login()

    def login(self):
        self.session.post(self.login_url, params={"principal": self.username, "password": self.password})

class ClearHarbor(object):
    
    def __init__(self, harbor_domain, password, schema="https",
                 username="admin"):
        self.schema = schema
        self.harbor_domain = harbor_domain
        self.harbor_url = self.schema + "://" + self.harbor_domain
        self.login_url = self.harbor_url + "/login"
        self.api_url = self.harbor_url + "/api"
        self.pro_url = self.api_url + "/projects"
        self.repos_url = self.api_url + "/repositories"
        self.username = username
        self.password = password
        self.client = RequestClient(self.login_url, self.username, self.password)

    def __fetch_pros_obj(self):
        # TODO
        self.pros_obj = self.client.session.get(self.pro_url).json()
        return self.pros_obj

    def fetch_pros_id(self):
        self.pros_id = []
        # TODO
        pro_res = self.__fetch_pros_obj()
        for i in pro_res:
            self.pros_id.append(i['project_id'])
        return self.pros_id

    def fetch_del_repos_name(self, pro_id):
        self.del_repos_name = []
        repos_res = self.client.session.get(self.repos_url, params={"project_id": pro_id})
        # TODO
        for repo in repos_res.json():
            if repo["tags_count"] > 30: 
                self.del_repos_name.append(repo['name'])
        return self.del_repos_name

    def fetch_del_repos(self, repo_name):
        self.del_res = []
        tag_url = self.repos_url + "/" + repo_name + "/tags"
        # TODO
        tags = self.client.session.get(tag_url).json()
        tags_sort = sorted(tags, key=lambda a: a["created"])
        #print(tags_sort) 
        del_tags = tags_sort[0:len(tags_sort) -30]
        #print(del_tags)
        for tag in del_tags:
            del_repo_tag_url = tag_url + "/" + tag['name']
            print(del_repo_tag_url)
            del_res = self.client.session.delete(del_repo_tag_url)
            self.del_res.append(del_res)

        return self.del_res


if __name__ == "__main__":
   
    harbor_domain = "hub.test.com" 
    password = "xxxxxxx"
    res = ClearHarbor(harbor_domain,password)
    # 循环所有的project id
    for i in res.fetch_pros_id():
        # 获取所有tag超过30的repos
        repos = res.fetch_del_repos_name(i)
        if repos:
            print(repos)   
            for repo in repos:
                del_repos = res.fetch_del_repos(repo)
                print(del_repos)

5.2、清理镜像释放空间

如开篇所说,通过ui清理镜像并不会真正将这些镜像从磁盘删除。要想真正释放磁盘空间,还得执行如下操作:

# 可以在参数中使用--dry-run以试运行(生产环境中还是尽量试运行下)
 docker run -it --name gc --rm --volumes-from registry vmware/registry:2.6.2-photon garbage-collect  /etc/registry/config.yaml

6、Harbor镜像迁移

6.1、背景说明

在早期生产环境尝试使用docker的时候,虽然使用了harbor作为镜像仓库,但是并没有做好相关存储规划,所有的镜像都直接存储到了harbor本地。随着业务发展,本地存储已无法满足镜像存储需求。

解决方案有两种:

  1. 使用共享文件系统存储,比如glusterfs,直接挂载本地的harbor存储目录当中。在此之前,只需要先把harbor本地目录中的文件拷贝到glusterfs当中即可。
  2. 部署一套新的harbor,直接使用共享存储作为镜像后端存储。将现有harbor中的所有镜像全量同步到新的harbor当中。

这两种方式操作起来从复杂度上来讲,都还好。但我们线上glusterfs面临下线。所以选择了第二种方式。

第二种方式,其实就是找一台新的机器,部署一套新的harbor,直接使用新的存储,无论是文件系统也好,对象存储也罢。然后使用harbor自带的主从复制即可完成镜像的全量同步。在此过程中,甚至不用停机。

但是,第二种方式,要求新部署的harbor版本与原harbor版本一致。我们原来的harbor版本比较低,而且很久没升级了,我这人有强迫症,觉得既然要搞,就干脆一步到位。直接使用最新版本的harbor部署了新的节点。这样一来,就没办法再使用原生的主从同步方式来完成镜像的同步了。

于是只好自己写脚本,基于harbor的rest api来完成镜像的导出与导入。

6.2、方案实现

先简单说下脚本的整执行流程:

  1. 先实现一个request来完成harbor的登录,获取session
  2. 获取所有的project
  3. 循环所有的project,获取所有的repositories
  4. 获取repositories的所有tag
  5. 根据repositories和tag拼接完整的镜像名称
  6. 连接两边的harbor,通过docker pull的方式从原harbor中拉取镜像,再通过docker push的方式将镜像推送到新harbor当中,然后删除本地镜像。
  7. 在上面的过程中,还做了个事情,每个镜像推送之后,都会将其镜像名称作为key,将其状态作为value保存到redis中。以备事后处理推送失败的镜像。

依赖组件:

  1. redis: 上面说了,依赖redis保存其推送状态

后续改进:

  1. 因为我这是在内网中跑,没有对http请求作任何校验,默认直接认为其成功,没做异常处理。
  2. 当前脚本可以开多个进程同时跑,以提供高好的性能。依赖redis对当前正在执行的镜像加锁。更好的方式,是在脚本中,直接以多进程的方式来实现。

下面直接上代码:

#!/usr/bin/env python
# -*- coding: utf-8 -*-


import requests
import subprocess
import json
import redis
import sys


class RequestClient(object):

    def __init__(self, login_url, username, password):
        self.username = username
        self.password = password
        self.login_url = login_url
        self.session = requests.Session()
        self.login()

    def login(self):
        self.session.post(self.login_url, params={"principal": self.username, "password": self.password})


class HarborRepos(object):

    def __init__(self, harbor_domain, harbor_new_domain, password, new_password, schema="https", new_schema="https",
                 username="admin", new_username="admin"):
        self.schema = schema
        self.harbor_domain = harbor_domain
        self.harbor_new_domain = harbor_new_domain
        self.harbor_url = self.schema + "://" + self.harbor_domain
        self.login_url = self.harbor_url + "/login"
        self.api_url = self.harbor_url + "/api"
        self.pro_url = self.api_url + "/projects"
        self.repos_url = self.api_url + "/repositories"
        self.username = username
        self.password = password
        self.client = RequestClient(self.login_url, self.username, self.password)

        self.new_schema = new_schema
        self.harbor_new_url = self.new_schema + "://" + self.harbor_new_domain
        self.login_new_url = self.harbor_new_url + "/c/login"
        self.api_new_url = self.harbor_new_url + "/api"
        self.pro_new_url = self.api_new_url + "/projects"
        self.new_username = new_username
        self.new_password = new_password
        self.new_client = RequestClient(self.login_new_url, self.new_username, self.new_password)

    def __fetch_pros_obj(self):
        # TODO
        self.pros_obj = self.client.session.get(self.pro_url).json()

        return self.pros_obj

    def fetch_pros_id(self):
        self.pros_id = []
        # TODO
        pro_res = self.__fetch_pros_obj()

        for i in pro_res:
            self.pros_id.append(i['project_id'])

        return self.pros_id

    def fetch_pro_name(self, pro_id):
        # TODO
        pro_res = self.__fetch_pros_obj()

        for i in pro_res:
            if i["project_id"] == pro_id:
                self.pro_name = i["name"]

        return self.pro_name

    # def judge_pros(self,pro_name):
    #    res = self.new_client.session.head(self.pro_new_url,params={"project_name": pro_name})
    #    print(res.status_code)
    #    if res.status_code == 404:
    #        return False
    #    else:
    #        return True

    def create_pros(self, pro_name):
        '''
        {
          "project_name": "string",
          "public": 1
        }

        '''
        pro_res = self.__fetch_pros_obj()
        pro_obj = {}
        pro_obj["metadata"]={}
        public = "false"
        for i in pro_res:
            if i["name"] == pro_name:
                pro_obj["project_name"] = pro_name
                if i["public"]:
                    public = "true"
                pro_obj["metadata"]["public"] = public
                # pro_obj["metadata"]["enable_content_trust"] = i["enable_content_trust"]
                # pro_obj["metadata"]["prevent_vul"] = i["prevent_vulnerable_images_from_running"]
                # pro_obj["metadata"]["severity"] = i["prevent_vulnerable_images_from_running_severity"]
                # pro_obj["metadata"]["auto_scan"] = i["automatically_scan_images_on_push"]
        headers = {"content-type": "application/json"}
        print(pro_obj)
        res = self.new_client.session.post(self.pro_new_url, headers=headers, data=json.dumps(pro_obj))
        if res.status_code == 409:
            print("\033[32m 项目 %s 已经存在!\033[0m" % pro_name)
            return True
        elif res.status_code == 201:
            # print(res.status_code)
            print("\033[33m 创建项目%s成功!\033[0m" % pro_name)
            return True
        else:
            print(res.status_code)
            print("\033[35m 创建项目%s失败!\033[0m" % pro_name)
            return False

    def fetch_repos_name(self, pro_id):
        self.repos_name = []

        repos_res = self.client.session.get(self.repos_url, params={"project_id": pro_id})
        # TODO
        for repo in repos_res.json():
            self.repos_name.append(repo['name'])
        return self.repos_name

    def fetch_repos(self, repo_name):
        self.repos = {}

        tag_url = self.repos_url + "/" + repo_name + "/tags"
        # TODO
        for tag in self.client.session.get(tag_url).json():
            full_repo_name = self.harbor_domain + "/" + repo_name + ":" + tag["name"]
            full_new_repo_name = self.harbor_new_domain + "/" + repo_name + ":" + tag["name"]
            self.repos[full_repo_name] = full_new_repo_name

        return self.repos

    def migrate_repos(self, full_repo_name, full_new_repo_name, redis_conn):
        # repo_cmd_dict = {}
        if redis_conn.exists(full_repo_name) and redis_conn.get(full_repo_name) == "1":
            print("\033[32m镜像 %s 已经存在!\033[0m" % full_repo_name)
            return
        else:
            cmd_list = []
            pull_old_repo = "docker pull " + full_repo_name
            tag_repo = "docker tag " + full_repo_name + " " + full_new_repo_name
            push_new_repo = "docker push " + full_new_repo_name
            del_old_repo = "docker rmi -f " + full_repo_name
            del_new_repo = "docker rmi -f " + full_new_repo_name
            cmd_list.append(pull_old_repo)
            cmd_list.append(tag_repo)
            cmd_list.append(push_new_repo)
            cmd_list.append(del_old_repo)
            cmd_list.append(del_new_repo)
            # repo_cmd_dict[full_repo_name] = cmd_list
            sum = 0
            for cmd in cmd_list:
                print("\033[34m Current command: %s\033[0m" % cmd)
                ret = subprocess.call(cmd, shell=True)
                sum += ret
            if sum == 0:
                print("\033[32m migrate %s success!\033[0m" % full_repo_name)
                redis_conn.set(full_repo_name, 1)
            else:
                print("\033[33m migrate %s faild!\033[0m" % full_repo_name)
                redis_conn.set(full_repo_name, 0)
            return


if __name__ == "__main__":
    harbor_domain = "hub.test.com"
    harbor_new_domain = "hub-new.test.com"
    re_pass = "xxxxxxx"
    re_new_pass = "xxxxxxx"
    pool = redis.ConnectionPool(host='localhost', port=6379,
                                decode_responses=True)  # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
    redis_conn = redis.Redis(connection_pool=pool)

    res = HarborRepos(harbor_domain, harbor_new_domain, re_pass, re_new_pass)
    # pros_id = res.fetch_pro_id()

    for pro_id in res.fetch_pros_id():
        #pro_id = 13
        pro_name = res.fetch_pro_name(pro_id)
        # print(pro_name)
        # ret = res.judge_pros(pro_name)
        # print(ret)
        res.create_pros(pro_name)
    #sys.exit() 
    for pro_id in res.fetch_pros_id():
        repos_name = res.fetch_repos_name(pro_id=pro_id)
        for repo_name in repos_name:
            repos = res.fetch_repos(repo_name=repo_name)
            for full_repo_name, full_new_repo_name in repos.items():
                res.migrate_repos(full_repo_name, full_new_repo_name, redis_conn)

你可能感兴趣的:(企业级镜像管理系统Harbor)