Harbor SSL避坑指南

Harbor的安装与配置请参考这里

Docker默认情况下,push和pull对应的仓库均需要ssl加密,如果我们在内网采用Harbor作为私仓,有两种方案可以解决:

  • Harbor开启ssl
  • docker侧修改配置文件,并且需重启docker服务

实际情况是,docker因为承载的服务较多,无法选择重启方案,那么就必须采用第一种方案。这种方案:

  • harbor侧开启ssl
  • 对应的docker服务侧需要拷贝对应的签名证书

Harbor开启ssl

创建ssl证书

1
2
3
4
5
6
7
8
9
创建一个证书文件夹
mkdir harbor_cert && cd harbor_cert
# 创建ca时,根据提示输入 IP地址
openssl req -newkey rsa:4096 -nodes -sha256 -keyout ca.key -x509 -days 365 -out ca.crt
openssl req -newkey rsa:4096 -nodes -sha256 -keyout server.key -out server.csr
# 把IP地址写入到文件中
echo subjectAltName = IP:xx.xx.xx.xx > extfile.cnf
# 生成 cr
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.cnf -out server.crt

配置Harbor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cd harbor2.3/
vim harbor.yml

#修改对应的配置项目:
# https related config
https:
# https port for harbor, default is 443
port: 10443
# The path of cert and key files for nginx
certificate: /root/harbor_cert/server.crt
private_key: /root/harbor_cert/server.key
#保存

#使配置生效
./prepare

#重启Harbor
docker-compose down -v
docker-compose up -v

拷贝对应签名文件到docker服务侧

1
2
3
scp server.crt  root@<docker侧主机>:/etc/docker/certs.d/<Harbor主机IP>:<harbor主机端口>/
scp ca.crt root@<docker侧主机>:/etc/docker/certs.d/<Harbor主机IP>:<harbor主机端口>/
scp server.key root@<docker侧主机>:/etc/docker/certs.d/<Harbor主机IP>:<harbor主机端口>/

这里注意的是:如果Harbor侧ssl的监听端口为443,则docker侧的路径为

1
/etc/docker/certs.d/<Harbor主机IP>/

否则为

1
/etc/docker/certs.d/<Harbor主机IP>:<harbor主机端口>/

测试

1
docker login <Harbor主机IP>:<harbor主机端口>

这样,无需重启docker服务,即可实现内网harbor私仓的正常使用。

相关的docker对应的官方文档

用DinD方式构建docke用DinD方式构建docker

Docker in Docker(dind)

Docker in Docker(dind) image可以用于Jenkins做build, 可以在一台机器上起多个docker image,每个image里面安装不同的第三方,形成不同的build环境,然后可以将待编译的代码SCP或GIT过去,用指定账号SSH来进行编译,达到一个机器多种编译环境的效果,提高了效率。Docker in Docker(dind) 镜像基于centos7,可用于jenkins打包编译:

  • 基础镜像: centos7
  • 内置用户:jenkinsbuild, 密码: jenkinsbuild, 构建文件夹: /home/jenkinsbuild/ci-jenkins
  • 采用ssh login方式登录

构建和使用镜像

构建镜像

1
./build-centos7-dind

展开容器

1
2
3
4
5
6
7
docker run -d -p 22 \
--name=centos7-dind \
-e TZ=Asia/Shanghai \
-e LANG=en_US.UTF-8 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /usr/bin/docker:/usr/bin/docker \
centos7-dind:1.0.0

将打代码copy到容器

1
scp -P \<port\> -r \<buildsourcecode\> jenkinsbuild@localhost:/home/jenkinsbuild/ci-jenkins/

ssh 登录进去

1
ssh -p \<port\> jenkinsbuild@localhost  

免密登录

1
将对对应公钥copy到 /home/jenkinsbuild/ 对应位置即可。

git地址

https://github.com/ryangsun/centos7-dind

Jenkins远程docker构建方案

背景介绍

Jenkins,包括idea,有很多集成方案构建docker,但是因为网络结构、构建环境等客观原因,集成性的方案并不能满足需求。如:

  • Jenkins权限限制;
  • Jenkins已经是docker化部署,无法嵌套集成方案;
  • 构建环境不唯一,需要多个docker环境分别构建;
  • Jenkins端无私仓权限,无法把构建好的docker部署到指定位置。

这时,基于DinD的构建方案就非常方便了。

安装与部署DinD容器

容器的Dockerfile见这里:

https://github.com/ryangsun/centos7-dind

安装和部署介绍见

docker构建环境搭建

免密登录

1、将jenkins对应使用的私钥copy到dind容器的对应文件中:

1
scp -P <DinD容器端口> <Jenkins私钥文件> <DinD容器>:/home/jenkinsbuild/.ssh/....

2、Jenkins创建新的远程主机连接

3、Jenkins SSH Publishers配置:

image-20210705115301268

第一步:针对一般的java项目,需要将jenkins打包好的jar文件和对应的Dockerfile拷贝到DinD容器。上图中,我们开了两个Transer Set,一个拷贝了.jar文件,一个拷贝了项目对应的Dockerfile。

第二步:远程执行命令构建docker:

1
2
3
4
5
6
#进入构建目录并构建Docker
cd ~/ci-jenkins/${JOB_NAME} && docker build -t <私仓IP>:<私仓端口>/mytest/${JOB_NAME}:${BUILD_ID} .
#将构建好的docker推到私仓
docker push <私仓IP>:<私仓端口>/mytest/${JOB_NAME}:${BUILD_ID}
#推私仓成功后,删除本地的docker image
docker rmi <私仓IP>:<私仓端口>/mytest/${JOB_NAME}:${BUILD_ID}

第三步:触发后续操作,如Rancher构建等。

Harbor的安装与配置

Harbor简介

Harbor 是由 VMware 公司中国团队为企业用户设计的 Registry server 开源项目,包括了权限管理(RBAC)、LDAP、审计、管理界面、自我注册、HA 等企业必需的功能,同时针对中国用户的特点,设计镜像复制和中文支持等功能。

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

Harbor的官网

1
https://goharbor.io/

Harbor的github地址

1
https://github.com/goharbor/harbor

(已经有15.2K的star了)

安装Harbor

前置条件

宿主机已安装好docker和docker-compose,具体安装方案这里不不展开。

下载&解压

Harbor有两个大版本,一是1.x,另外是2.x,据说2.x优化了很多,所以这里采用2.3.0这个版本,下载离线安装包。

1
2
3
wget https://github.com/goharbor/harbor/releases/download/v2.3.0/harbor-offline-installer-v2.3.0.tgz
tar zxvf harbor-offline-installer-v2.3.0.tgz
cd harbor

修改配置文件

将配置模板copy一份

1
cp harbor.yml.tmpl harbor.yml

编辑配置模板

1
vim harbor.yml

主要修改以下几个地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# The IP address or hostname to access admin UI and registry service.
# DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
hostname: <你的harbor的域名或者IP(不加端口号)>

# http related config
http:
# port for http, default is 80. If https enabled, this port will redirect to https port
port: <http协议监听的端口>

# https related config
https:
# https port for harbor, default is 443
port: <https协议监听的端口>
# The path of cert and key files for nginx
certificate: <.crt证书文件的路径>
private_key: <.key证书文件的路径>

保存后执行配置脚本,使刚才的配置生效

执行配置脚本

1
./prepare

安装

1
./install.sh

如果之后修改配置再次启动,此步骤可以省略。

安装成功后,即可在 https://youip 上访问harbor了,初始帐号为 admin 初始密码为 Harbor12345 。

一些常用命令

启动Harbor

1
docker-compose up -d

关闭Harbor

1
docker-compose down -v

修改配制后需要重新关停和开启Harbor

1
2
3
./prepare #执行配置脚本
docker-compose down -v #关停Harbor
docker-compose up -d #启动Harbor

语雀javaSDK

支持语雀全部api接口,纯java编写,项目已上传至maven中央仓库,可直接maven引用。

源码地址

包引用

<dependency>
    <groupId>com.bumao.models.yuquesdk</groupId>
    <artifactId>yuquesdk</artifactId>
    <version>0.0.1</version>
</dependency>

使用方式

YuqueClient client = new YuqueClient("yuque-token");

System.out.println("--获取某个用户信息 by Id--");
UserDetailSerializer userDetailSerializer = client.getUserInfoById("1757692");
System.out.println(userDetailSerializer);

System.out.println("--获取某个用户信息 by Login--");
UserDetailSerializer user1 = client.getUserInfoByLogin("zhangxin_ux");
System.out.println(user1);
System.out.println(JSONObject.toJSONString(user1) );

System.out.println("--获取当前用户信息--");
UserDetailSerializer user2 = client.getCurrUserInfo();
System.out.println(user2);

System.out.println("--获取某个用户的加入的组织列表 by Id--");
List<GroupSerializer> groupList = client.listJoinedGroupById("1757692");
System.out.println(groupList);

System.out.println("--获取某个用户的加入的组织列表 by Login--");
List<GroupSerializer> groupList1 = client.listJoinedGroupByLogin("u1486894");
System.out.println(groupList1);

System.out.println("--获取公开组织列表--");
List<GroupSerializer> groupList3 = client.listPublicGroup();
System.out.println(groupList3);
System.out.println(groupList3.size());

List<GroupSerializer> groupList2 = client.listPublicGroup(99);
System.out.println(groupList2);
System.out.println(groupList2.size());

System.out.println("--获取单个组织的详细信息--");
GroupDetailVo groupDetailVo = client.getGroupInfoById("antv");
System.out.println(groupDetailVo.getData());
System.out.println(groupDetailVo.getAbilities());

System.out.println("--创建组织--");
GroupCreatePo createPo = new GroupCreatePo();
createPo.setName("firstGroup1");
createPo.setLogin("firstGroup2");
createPo.setDescription("firstGroup");
try {
    GroupSerializer newGroup = client.createGroup(createPo);
    System.out.println(newGroup);
}catch (YuqueException e){
    System.out.println(e);
    System.out.println("创建组织错误:httpCode="+e.getHttpCode()+",code="+e.getOrigCode()+",message="+e.getOrigMsg());
}

System.out.println("--更新组织--");
GroupCreatePo updatePo = new GroupCreatePo();
updatePo.setName("first被更新");
updatePo.setLogin("firstGroup2");
updatePo.setDescription("1234567890");
try {
    GroupSerializer newGroup = client.updateGroupById("21807875",updatePo);
    System.out.println(newGroup);
}catch (YuqueException e){
    System.out.println(e);
    if(e.getHttpCode().equals(400)) {
System.out.println("更新组织错误:httpCode=" + e.getHttpCode() + ",code=" + e.getOrigCode() + ",message=" + e.getOrigMsg());
    }
}

System.out.println("--删除组织--");
try {
    client.deleteGroupById("21807875");
}catch (YuqueException e){
    System.out.println(e);
}

System.out.println("--获取组织成员信息--");
List<GroupUserSerializer> groupUserSerializerList = client.listGroupUserById("firstGroup");
System.out.println(groupUserSerializerList);

System.out.println("--增加或更新组织成员--");
GroupUserSerializer UpgroupUserSerializer = client.updateGroupUserbyId("firstGroup","zhangxin_ux",1);
System.out.println(UpgroupUserSerializer);

System.out.println("--删除组织成员--");
GroupUserSerializer DelgroupUserSerializer = client.deleteGroupUserbyId("firstGroup","zhangxin_ux");
System.out.println(DelgroupUserSerializer);

System.out.println("--获取某个用户的知识库列表 by Id--");
List<BookSerializer> bookSerializerList = client.listUserReposById("u1486894","all",0);
System.out.println(bookSerializerList);

System.out.println("--获取某个团队的知识库列表 by Id--");
List<BookSerializer> groupbookSerializerList = client.listGroupReposById("firstgroup","all",0);
System.out.println(groupbookSerializerList);

System.out.println("--往团队创建知识库 by Id--");
RepoCreatePo createPo = new RepoCreatePo();
createPo.setDescription("介绍");
createPo.setName("名称");
createPo.setPublic_id(1);
createPo.setSlug("abc");
createPo.setType("Book");
BookSerializer bookSerializer = client.createGroupRepoById("firstgroup",createPo);
System.out.println(bookSerializer);

System.out.println("--往自己下面创建知识库 by Id--");
RepoCreatePo cPo = new RepoCreatePo();
cPo.setDescription("介绍");
cPo.setName("名称");
cPo.setPublic_id(1);
cPo.setSlug("abc");
cPo.setType("Book");
BookSerializer bookSerializer = client.createUserRepoById("u1486894",cPo);
System.out.println(bookSerializer);

System.out.println("--获取知识库详情 by Id--");
BookDetailVo bookDetailVo = client.getRepoDetailById("20113349");
System.out.println(bookDetailVo.getData());//repo
System.out.println(bookDetailVo.getAbilities());//Abilities

System.out.println("--更新知识库信息 by repoId--");
RepoCreatePo upPo = new RepoCreatePo();
upPo.setDescription("介绍");
upPo.setName("名称");
upPo.setPublic_id(0);
upPo.setSlug("abc");
upPo.setType("Book");
BookSerializer bookSerializerUp = client.updateRepoById("20167466",upPo);
System.out.println(bookSerializerUp);

System.out.println("--删除知识库 by repoId--");
BookSerializer bookSerializerDel = client.deleteRepoById("u1486894/slug");
System.out.println(bookSerializerDel);

System.out.println("--获取某仓库的目录 by nameSpace--");
List<TocSerializer> tocSerializerList = client.listTocByNameSpace("u1486894/nn3k9e");
System.out.println(tocSerializerList);

System.out.println("--获取一个仓库的文档列表 by nameSpace--");
List<DocSerializer> docSerializerList = client.listDocByNameSpace("20113349");
System.out.println(docSerializerList);

System.out.println("--获取单篇文档的详细信息 by slug--");
DocDetailVo docDetailVo = client.getDocDetailBySlug("u1486894/nn3k9e","hgg9lg");
System.out.println(docDetailVo.getData());//文章
System.out.println(docDetailVo.getAbilities());//Abilities

System.out.println("--创建文档 by nameSpace--");
DocCreatePo createPo = new DocCreatePo();
createPo.setTitle("helloword");
createPo.setSlug("helloword1");
createPo.setPublic_id(1);
createPo.setFormat("markdown");
createPo.setBody("#helloword");
DocSerializer docSerializerCreate = client.createDocByNameSpace("u1486894/nn3k9e",createPo);
System.out.println(docSerializerCreate);

System.out.println("--更新文档 by nameSpace--");
DocCreatePo upPo = new DocCreatePo();
upPo.setTitle("helloword");
upPo.setSlug("helloword1");
upPo.setPublic_id(1);
upPo.setFormat("markdown");
upPo.setBody("# helloword11111111");
DocSerializer docSerializerUp = client.updateDocByNameSpace("u1486894/nn3k9e","46939700",upPo);
System.out.println(docSerializerUp);

System.out.println("--删除文档 by repoId--");
DocSerializer docSerializerDel = client.deleteDocByRepoId("20113349","46939326");
System.out.println(docSerializerDel);

System.out.println("--搜索--");
SearchPo searchPo = new SearchPo();
searchPo.setType("doc");
searchPo.setQ("世界上最好的语言");
JSONObject obj = client.listSearch(searchPo);
System.out.println(obj);

Table2Entry将建表语句转义为实体

Table2Entry可以方便的将mysql建表语句转化为实体对象,进而帮助你快速映射实体。

Table2Entry源码地址

包引用

正常使用

<dependency>
    <groupId>com.bumao.model</groupId>
    <artifactId>table2entry</artifactId>
    <version>0.0.1</version>
</dependency>

包冲突的处理

<dependency>
    <groupId>com.bumao.model</groupId>
    <artifactId>table2entry</artifactId>
    <version>0.0.1</version>
    <exclusions>
        <!--druid-->
        <exclusion>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </exclusion>
        <!--lombok-->
        <exclusion>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </exclusion>
        <!--slf4j-api-->
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Table2Entry的使用

String sql = "CREATE TABLE `card` (\n" +
        "  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'PK',\n" +
        "  `batch_id` int(11) NOT NULL COMMENT '所属批次ID',\n" +
        "  `denomination` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '面额',\n" +
        "  `card_no` varchar(20) NOT NULL COMMENT '实体卡号',\n" +
        "  `card_pass` varchar(20) NOT NULL COMMENT '实体密码',\n" +
        "  `status` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '状态,0=未核销 1=已核销',\n" +
        "  `active_time` datetime DEFAULT NULL COMMENT '核销核销时间',\n" +
        "  `active_userid` varchar(32) DEFAULT NULL COMMENT '核销用户ID',\n" +
        "  `active_mobile` varchar(20) DEFAULT NULL COMMENT '核销时用户的手机号码',\n" +
        "  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n" +
        "  `stop_time` datetime NOT NULL COMMENT '结束核销时间',\n" +
        "  `active_openid` varchar(64) DEFAULT NULL COMMENT '用户openid',\n" +
        "  PRIMARY KEY (`id`) USING BTREE,\n" +
        "  KEY `batch_id` (`batch_id`) USING BTREE,\n" +
        "  KEY `status` (`status`) USING BTREE\n" +
        ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡券批次表';" +
        "select * from card;";
ToEntry toEntry = new ToEntry();
List<TableEntryVo> tableEntryVos = toEntry.getEntry(sql);
log.info("tableEntryVos={}", JSONArray.toJSON(tableEntryVos));
  • 可同时转义多条建表语句,请用“;”隔开。
  • 会忽略建表语句以外的sql。

TODO

针对Mysql建表语句的转义

针对Oracle建表语句的转义

针对MsSQL建表语句的转义

当Mysql遇到SSL

SpringBoot 连接提示 Communications link failure

1
2
3
4
5
6
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure

The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174) ~[mysql-connector-java-8.0.20.jar:8.0.20]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64) ~[mysql-connector-java-8.0.20.jar:8.0.20]
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:836) ~[mysql-connector-

具体的错误原因没找到,只从网上搜到了这个:

1
原因是MySQL在高版本需要指明是否进行SSL连接
1
2
Establishing SSL connection without server’s identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn’t set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to ‘false’. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
不建议在没有服务器身份验证的情况下建立SSL连接。根据MySQL 5.5.45+、5.6.26+和5.7.6+的要求,如果不设置显式选项,则必须建立默认的SSL连接。需要通过设置useSSL=false来显式禁用SSL,或者设置useSSL=true并为服务器证书验证提供信任存储。

用参数 useSSL=true 进行尝试,发现还是报一样的错误,当使用 useSSL=false 时,就可以进行连接了。也就是

1
jdbc:mysql://192.168.221.201:3306/jdbc?useSSL=false

不过为什么要这样连接,暂时没弄清楚,只知道时版本和兼容的原因。